Клиент: Apollo Client и кэш
На фронтенде запросы отправляет GraphQL-клиент. Apollo Client не просто шлёт запросы — он нормализует ответы в кэш по идентификаторам и отдаёт данные мгновенно при повторе.
В REST за тебя кэширует браузер по URL. В GraphQL URL один — поэтому умный кэш переезжает на клиента, и это его суперсила.
Технически отправить GraphQL-запрос можно обычным fetch — это просто POST с JSON. Но на практике используют клиентские библиотеки: Apollo Client, urql, Relay. Они берут на себя кэш, повторные запросы, обновление UI и работу с переменными. Самый популярный — Apollo Client. Стоит знать и про различия: urql по умолчанию использует более простой документный кэш (ответ кэшируется целиком по паре «запрос + переменные»), тогда как Apollo Client и Relay строят нормализованный кэш по идентификаторам объектов. Документный кэш проще и отлично подходит для несложного CRUD; нормализованный сложнее, но окупается на приложениях с богатыми связями, оптимистичными обновлениями и общими между экранами сущностями. Выбор клиента — это во многом выбор стратегии кэширования под твой тип приложения.
Нормализованный кэш
Главная фишка Apollo Client — нормализованный кэш. Он не хранит ответы как есть, а разбирает их на объекты по ключу __typename + id и складывает в плоский словарь. Поэтому один и тот же пользователь, пришедший в двух разных запросах, в кэше лежит один раз. Обновил его в одном месте — обновилось везде.
Ответ запроса A Нормализованный кэш
user(1) { name } -> "User:1": { id:1, name:"Аня" }
Ответ запроса B "Post:7": { id:7, title:"...",
post(7){author{name}} -> author: ref("User:1") }
^ ссылка, а не копия!
Когда придёт мутация, обновившая имя User:1, оба места (запрос A и автор поста из запроса B) покажут новое имя — потому что это одна запись в кэше.
Переменные и ключ кэша
Результат запроса кэшируется по документу запроса + переменным. Запрос с id: "1" и тот же запрос с id: "2" — это разные ключи кэша. Поэтому так важно выносить значения в переменные (как мы учили в разделе про запросы): по ним строится ключ кэша.
Как работает под капотом
При запросе клиент сначала проверяет кэш: есть данные под этим ключом — отдаёт мгновенно (cache hit), нет — идёт по сети, кладёт ответ в нормализованный кэш и только потом отдаёт UI. Смоделируем нормализацию и попадание в кэш:
const cache = new Map();
// нормализация: раскладываем объекты по ключу __typename:id
function writeToCache(obj) {
const key = obj.__typename + ":" + obj.id;
cache.set(key, { ...cache.get(key), ...obj });
return key;
}
let networkCalls = 0;
function fetchUser(id) { // "поход по сети"
networkCalls++;
return { __typename: "User", id, name: "Аня" };
}
function getUser(id) {
const key = "User:" + id;
if (cache.has(key)) return { from: "cache", data: cache.get(key) };
const data = fetchUser(id);
writeToCache(data);
return { from: "network", data };
}
console.log(getUser(1)); // network — кэш пуст
console.log(getUser(1)); // cache — мгновенно, без сети!
console.log("Походов по сети:", networkCalls); // 1, а не 2
Попробуй сам ▶ — запроси getUser(2): это новый ключ, будет поход по сети, а затем повтор getUser(2) уже из кэша. Так клиент экономит запросы.
Частые ошибки
- Ждать, что HTTP-кэш «сам справится». GraphQL — это POST в один URL, браузерный кэш по URL тут бесполезен. Кэшем управляет клиент.
- Объекты без id. Нормализация опирается на идентификатор. Если у типа нет
id, Apollo не сможет нормализовать его и обновления «расползутся». - Забыть обновить кэш после мутации. Создал новый объект мутацией — его может не быть в кэше списка; иногда кэш нужно поправить вручную или перезапросить.
Best practices
- Всегда запрашивай
idу объектов, которые должны кэшироваться — это топливо нормализации. - Используй переменные: они формируют ключ кэша и делают запросы переиспользуемыми.
- Для простого CRUD хватает документного кэша (как у urql); нормализованный кэш Apollo окупается на сложных связях, оптимистичных обновлениях и пагинации.
Итоги
GraphQL-клиент (чаще всего Apollo Client) отправляет запросы и хранит ответы в нормализованном кэше по ключу __typename:id: один объект — одна запись, обновил раз — обновилось везде. Ключ кэша строится из запроса и переменных, а HTTP-кэш браузера здесь не работает, потому что эндпоинт один. Дальше соберём всё в финальные best practices.