Запрос за списком заказов, сделанный компонентом, повторяется при каждом его монтировании, а страница с тремя компонентами про одни и те же сведения гонит три запроса к одному эндпоинту. Hook-библиотека SWR переносит в React идею из протокола кеширования: черствое отдаётся мгновенно из кеша, свежесть наводится в фоне, и пользователь видит экран сразу, не дожидаясь сети. Название расшифровывается как показывай черствое, обновляй в тишине, и вся библиотека это воплощение одной стратегии кеширования, доведённой до hook-эргономики.

Статья разбирает идею и механику: стратегию кеша с фоновой ревалидацией, ключи и автоматические триггеры обновления, локальные мутации с оптимистичным экраном, пагинацию без прыжков, честную работу с ошибками и границы, за которыми SWR не нужен. С кодом на библиотеку и числами настроек по умолчанию, которые проверяются документацией за минуту.

Идея stale-while-revalidate показать старое и тихо обновить

Классический запрос в React живёт от загрузки до загрузки: компонент монтируется, запрос стартует, экран ждёт. Стратегия черствого обновления меняет порядок: компонент монтируется, мгновенно получает последние известные значения из кеша, сеть работает в фоне, и при ответе экран обновляется на свежее. Пользователь видит содержимое сразу, свежесть приезжает вторым кадром, и для повторных визитов это ощущается как мгновенный интерфейс.

Стратегия честна про границы свежести: значения в кеше называются черствыми, потому что им назначен возраст, и черствое это не порча, а состояние, требующее сверки. Пользователь, увидевший цену из кеша секундной давности, получает правду, и правда обновляется сама, без его участия и без явной перезагрузки.

Доля сценариев, где старое лучше пустоты, огромна: личные кабинеты, ленты, справочники, любые повторные экраны. Исключения тоже честны: экран платежа перед подтверждением обязан показывать только свежее, и для таких мест стратегия отключается или заменяется ожиданием загрузки, потому что мгновенность здесь дешевле точности.

Идея старше библиотеки и принадлежит протоколам кеширования, а библиотека перенесла её в мир компонентов: каждый компонент объявляет свой ключ и свою функцию запроса, всё остальное кеш, дедупликация, обновление и повторные попытки делает обвязка. Компонентный код читается как декларация о том, что нужно, а не как инструкция о том, как достать.

Хук useSWR ключи кеша и автоматика обновления

Хук принимает ключ и загрузчик: ключ это строка или массив, идентифицирующая запись в глобальном кеше, загрузчик это функция превращения ключа в обещание ответа. Все компоненты, запросившие один ключ, делят одну запись кеша и один сетевой запрос, и дедупликация с интервалом по умолчанию около двух секунд гасит залпы монтирования целыми.

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((r) => r.json());

function Profile() {
  const { data, error, isLoading } = useSWR("/api/user", fetcher);

  if (isLoading) return <Skeleton />;
  if (error) return <Failed />;
  return <Card user={data} />;
}

Кеш глобален по приложению и живёт вне компонентов: размонтирование компонента не выбрасывает значения, и возврат на экран получает их мгновенно с тихой ревалидацией. Значения в кеше переживают переходы, и навигация по приложению перестаёт быть чередой скелетонов для всего, что уже видели.

Ключи строятся по смыслу записи, а не по месту использования: эндпоинт с параметрами образует составной ключ массивом, и одинаковые записи из разных экранов совпадают в кеше автоматически. Дисциплина ключей это дисциплина идентичности: один и тот же ресурс обязан называться одинаково везде, иначе кеш распадается на копии, и дедупликация молча умирает.

Загрузчик остаётся обычной функцией: обёртка над встроенным запросом, клиент с токенами, GraphQL-клиент, и библиотека не диктует транспорт. Ответ заворачивается в значения хука: последние значения, ошибка, состояние загрузки, и компонент рисует все три состояния явно, без импровизаций на тему успела или нет.

Триггеры ревалидации фокус возврат сети и интервалы

Ревалидация это сверка кеша с источником, и библиотека назначает её автоматически по событиям, о которых страница знает лучше пользователя. Возврат фокуса на вкладку запускает сверку, потому что человек вернулся и заслуживает свежего. Возврат сети запускает сверку, потому что сеть была недоступна и кеш наверняка устарел. Оба триггера включены по умолчанию и настраиваются порогами, чтобы частые переключения вкладок не превращались в частые сетевые залпы.

Интервальная ревалидация добавляет свежесть по расписанию: экран с курсами и статусами получает сверку каждые несколько секунд, интервал задаётся настройкой на ключ, и троттлинг сглаживает жадные интервалы при невидимости страницы. Живой экран обновляется, невидимый не трудится, и сочетание фокуса, сети и интервала закрывает большинство сценариев обновления без единой строки кода в компонентах.

Ручные триггеры дополняют автоматические: глобальная перезагрузка всех записей после входа в аккаунт, точечная перезагрузка одного ключа после известного изменения, и такие операции живут в обработчиках событий, а не в таймерах. Автоматика снимает рутину, ручные вызовы снимают особые случаи, и зрелая страница использует оба слоя осознанно.

Мутации локальные обновления и оптимистичный экран

Сверка читает, а мутация пишет, и библиотека даёт две стороны одной операции: локальное обновление кеша без сети и запуск ревалидации после успешного изменения на сервере. Локальная функция обновления берёт текущие значения ключа и возвращает изменённые, экран перекрашивается мгновенно, а сетевая операция подтверждает изменение в тишине, и на подтверждении кеш наводится на правду.

Оптимистичный сценарий собирается из этих половин: экран меняется сразу, запрос идёт, при отказе экран откатывается и пользователю показывается честное состояние неудачи. Знакомая механика из статей об интерфейсах повторяется здесь на уровне кеша, и её достоинство то же: ощущение мгновенности при честном откате, а не вращающийся спиннер вместо ответа.

const { data: orders, mutate } = useSWR("/api/orders", fetcher);

async function renameOrder(id, title) {
  await mutate(
    async () => {
      const next = await fetcher("/api/rename", {
        method: "POST",
        body: JSON.stringify({ id, title })
      });
      return next;
    },
    {
      optimisticData: orders.map((o) =>
        o.id === id ? { ...o, title } : o
      ),
      rollbackOnError: true
    }
  );
}

Мутация с оптимистичными значениями описывается настройками: будущий экран для мгновенного отклика, откат при ошибке, перезагрузка после записи. Код компонента остаётся коротким, потому что порядок мгновенно-сеть-откат это шаблон, а не импровизация каждого обработчика.

Дисциплина ключей возвращается в мутациях с новой силой: изменение, затрагивающее несколько экранов, обязано пометить все затронутые ключи, иначе один экран обновится, второй останется с черствым прошлым. Карта зависимостей ресурсов это таблица, которую зрелая команда держит явной, и мутация сверяется с ней, а не с памятью разработчика о существовании экранов.

Пагинация и списки keepPreviousData и плавные переходы

Пагинация это классическое место, где кеш либо помогает, либо скачет: переход на вторую страницу с чистым кешем это скелетон вместо списка, и прыжки между страницами превращают таблицу в мигающий аттракцион. Настройка удержания предыдущих значений решает прыжки: при смене ключа страницы экран продолжает показывать прежний список, помечая состояние подгрузкой, и смена страницы ощущается перелистыванием, а не занавесом.

const { data: orders } = useSWR(
  `/api/orders?page=${page}`,
  fetcher,
  { keepPreviousData: true }
);

Предзагрузка следующей страницы дополняет плавность: соседняя страница запрашивается заранее молча, и клик по следующей находит значения уже в кеше, мгновенно. Комбинация удержания и предзагрузки собирает pagination высшей пробы, и обе опции это строчки настройки, а не архитектура.

Бесконечная лента живёт своим режимом: страницы накапливаются как последовательные записи, добавление следующей порции работает с состоянием накопления, и списки с десятками страниц остаются предсказуемыми. Ключи страниц строятся по номеру или курсору, курсорные ленты честнее нумерованных при изменяющихся источниках, и выбор курсора снимает классические дубли на границах порций.

Сортировка и фильтры живут в ключе: изменение фильтра это новый ключ, и кеш хранит результаты каждого фильтра отдельно, возврат к прежнему фильтру мгновенен. Удержание прежних значений при смене фильтра убирает прыжки, предзагрузка частых фильтров ускоряет массового пользователя, и весь набор это настройки на ключах, растущих из смысла запроса.

Ошибки повторы и границы между кешем и черствым

Ошибка запроса это состояние записи, а не компонента: неудавшийся ключ сохраняет ошибку, компоненты этого ключа рисуют её одинаково, и повторы идут с нарастающими паузами по умолчанию. Порог повторов и профиль пауз настраиваются на ключе, чувствительные операции получают меньше упорства, чтение справочников получает больше, и выбор это снова решение на смысл записи.

Кеш с ошибкой и кеш с черствым различаются честно: черствое показывается, ошибочное по умолчанию не показывается вместо значений, потому что показать несостоявшееся как правду хуже, чем показать состояние неудачи. Компонент обязан рисовать обе ветки, и вежливость веток это часть контракта хука: явный экран ошибки с действием повторить вместо тихого застревания на прошлом.

Возраст кеша становится частью интерфейса там, где честность свежести важна: метка времени последней успешной синхронизации, лёгкая пометка черствости на фоне обновления, и пользователь всегда знает, смотрит он в секунду или в прошлый вечер. Датированный кеш это доверие дешевле десятка мест, где дата отсутствует, а нужна именно она.

Граница применимости SWR проходит по характеру сведений: записи, которые читаются и периодически сверяются, это жанр библиотеки, диалоги форм и поток ввода это жанр состояния компонентов, тяжёлые интерактивные подписки это жанр отдельных решений. SWR не занимает трон всего состояния приложения, он занимает трон сетевых записей, и смеси жанров от этого становятся чище, а не запутаннее.

SWR против рукописных решений и соседей жанра

Рукописное управление кешем это знакомый конструктор из контекста, редьюсеров и кучи флагов загрузки, и его цена известна каждой команде, вырастившей свой велосипед: дедупликация пишется, фокус обрабатывается, повторы изобретаются, и через год велосипед обгоняет бизнес-логику по объёму. Библиотека жанра снимает конструктор целиком, оставляя компонентам декларацию о нужных записях, и экономия кода это лишь половина выгоды, вторая половина это предсказуемость общих решений.

Сосед по жанру, библиотека запросов с уклоном в серверное состояние, даёт ту же стратегию с собственными акцентами: больше встроенной механики мутаций, своё сообщество и свои дефолты. Выбор между соседями это выбор вкуса команды и существующей кодовой базы, обе линейки зрелые, и миграция между ними при необходимости спокойнее, чем кажется, потому что слой абстракции одинаков по духу: ключи, кеш, ревалидация.

Порядок внедрения SWR в приложение свёрнут в короткую лестницу:

  1. завести единый загрузчик и дисциплину ключей по смыслу записей;
  2. перевести повторяющиеся чтения на хук с дефолтами без настройки;
  3. включить удержание прежних значений на пагинациях и фильтрах;
  4. добавить предзагрузку для частых переходов и горячих списков;
  5. собрать оптимистичные мутации с откатом и перезагрузкой затронутых ключей;
  6. оформить ветки ошибок и метки свежести на экранах, где возраст важен;
  7. сверить карту ресурсов и их зависимостей раз в квартал на ревью.

SWR это та редкая библиотека, которая обещает меньше, чем даёт: описание занимает абзац, а эффект измеряется исчезновением целого класса кода про ожидания, повторы и гонки монтирования. Экран, который показывает то, что знает, и тихо узнаёт больше, и есть интерфейс, который не заставляет пользователя думать о сетевых происшествиях, и за такое молчание интерфейсы и любят.