Большинство фронтенд-кода воспринимает асинхронную работу как нечто однородное. Вы запускаете Promise, ждете его разрешения, записываете результат в локальное состояние и позволяете фреймворку сопоставить изменения. Этот паттерн соблазнителен, потому что он работает везде: REST-вызов, отправка формы, сообщение через WebSocket. Все они попадают в один и тот же useEffect или обработчик событий, проходят через setState и выглядят как идентичная асинхронная «разводка». Это единообразие — ловушка. В реальном приложении не вся асинхронная работа одинакова. Притворяться, что это так, значит превращать ваши UI-компоненты в случайных архитекторов данных, склеенных с помощью хуков useEffect и надежды на удачу.

На самом деле асинхронные операции делятся на три разных вида. У каждого из них свои отношения со временем, кэшированием и владением. Умение различать их — это то, что позволяет фронтенду оставаться быстрым, корректным и предсказуемым.

Queries: Факты с адресом

Запрос (query) — это не просто получение данных (fetch). Это запрос идентифицируемого факта. Вы запрашиваете /user/123, а не «какие-то данные пользователя». Это различие важно, потому что именно идентичность делает кэширование возможным. Если двум компонентам на одном экране нужны одни и те же данные пользователя, они должны получать один и тот же ответ. Когда каждый компонент хранит свою локальную копию в useState, вы дробите истину. Аватар в хедере и имя в сайдбаре начинают расходиться, потому что они были получены в разное время, или один запрос завершился ошибкой, а другой — успешно.

Думайте о запросе как о ресурсе, а не об действии. У него есть ключ кэша, политика свежести и жизненный цикл, который длится дольше, чем любой отдельный компонент. Хорошо выстроенный слой запросов понимает, что чтение /projects?page=2 отличается от чтения /projects?page=3. Каждый URL и набор параметров формирует адрес, и данные по этому адресу могут быть устаревшими, свежими или отсутствовать. UI не должен заниматься этим учетом. Он должен запрашивать у слоя данных user:123 и получать снимок (snapshot). Пришло ли это значение с сервера две секунды назад или из кэша две миллисекунды назад — компоненту не должно быть до этого дела.

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

Mutations: Изменение мира

Если запросы спрашивают о мире, то мутации его меняют. Сам сетевой запрос — POST, PUT или DELETE — обычно является самой простой частью. Сложная часть — это всё, что происходит после того, как сервер отвечает «OK».

Представьте, что пользователь обновляет свое отображаемое имя. Сама мутация — это один запрос. Но «радиус поражения» охватывает всё приложение. Страница профиля хранит старое имя. Панель навигации показывает старое имя. История комментариев может ссылаться на него. Если ваш код мутации делает ничего, кроме переключения флага isLoading и обновления одной части состояния, ваше приложение начинает лгать самому себе. Одни части UI делают вид, что изменения произошли. Другие даже не подозревают, что что-то изменилось.

Мутация должна объявлять о своем влиянии на граф данных. Она должна сообщать системе, какие запросы теперь невалидны, какие ключи кэша нужно обновить и какие связи изменились. Это принципиально отличается от запроса. Запрос предназначен только для чтения и доступен для совместного использования. Мутация ориентирована на запись и разрушает существующие кэши. Смешивание их в одной абстракции приводит к тому, что разработчики вынуждены вручную вызывать refetch() в случайных компонентах или, что еще хуже, разбрасывать хуки useEffect по всему дереву, чтобы «синхронизировать» локальное состояние с сервером.

Модель владения также отличается. Запросами обычно владеет кэш. Мутацией владеет пользовательское действие, которое ее инициировало. У нее есть состояние ожидания (pending), состояние ошибки и, возможно, оптимистичное значение, которое необходимо откатить