React data/state: 17 сложных вопросов с объяснением для собеседования

17 сложных вопросов по React data/state: query key, invalidation, optimistic update, Zustand, URL state, drafts форм и сильные ответы для middle-интервью.

14 апреля 2026 г.22 минLexicon Team

Введение

На React-собеседовании тема data/state быстро отделяет человека, который просто знает API, от человека, который умеет проектировать экран целиком. Интервьюер редко интересуется только тем, знаете ли вы useQuery, useState или Zustand. Обычно он проверяет несколько ключевых моментов: понимаете ли вы, кто владеет данными, где проходит граница между UI и сервером и как данные устаревают. Также важно объяснить, почему после mutation интерфейс расходится с бэкендом и где именно команда создала второй источник истины.

В подборке React-материалов это особенно хорошо видно по статьям про client state vs server state в React, React data fetching паттерны, React Query под капотом и когда использовать Zustand. Эта статья не дублирует их, а собирает 17 интервью-вопросов вокруг одной темы: как строить data flow так, чтобы он переживал рост продукта, refetch, mutation, навигацию и живых пользователей.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью

Подписаться

Как читать эту подборку

Ниже вопросы сгруппированы не по библиотекам, а по инженерным решениям.

БлокЧто проверяютГде кандидаты чаще всего теряют баллы
Источник истиныПонимаете ли вы, кто владеет даннымисмешивают URL, local state и кэш запросов
Cache designУмеете ли вы проектировать query key и invalidationДелают ключи слишком общими или слишком дробными
Mutation flowЗнаете ли вы, что происходит после записиЗабывают rollback, conflict и пересинхронизацию
Shared client stateОтличаете ли store от server cacheКладут ответ API в Zustand или Context
Forms and draftsПонимаете ли вы ownership формыПерезатирают пользовательский ввод refetch-ом
ScalingМожете ли вы держать data flow на длинной дистанцииЛечат симптомы локальными патчами

Архитектурный разбор: где обычно живут данные в зрелом React-приложении

На одном экране у вас почти всегда несколько типов состояния сразу:

  • local UI state: открыт ли dropdown, какой таб активен, какой input в фокусе;
  • URL state: фильтры, пагинация, сортировка, которые должны пережить reload и шаринг ссылки;
  • server state: список заказов, профиль, права доступа, результаты поиска;
  • draft state: локальная рабочая копия формы;
  • shared client state: selection, pinned items, multi-step workflow, UI-shell между несколькими зонами интерфейса.

Проблемы начинаются не тогда, когда какого-то слоя нет, а когда два слоя объявляются владельцами одних и тех же данных. Например, список заказов уже живет в query cache, но его еще кладут в Zustand "для удобства". Или фильтр одновременно хранится в URL и в локальном store. Или форма редактирует server state напрямую, а потом refetch неожиданно перезаписывает draft.

Рабочая схема обычно выглядит так:

  1. URL владеет shareable-состоянием.
  2. Query cache владеет снимком server state и его жизненным циклом.
  3. Draft живет локально, пока пользователь редактирует форму.
  4. Store вроде Zustand владеет только shared client state, которое не принадлежит серверу.
  5. После mutation вы либо точечно обновляете cache, либо делаете контролируемую invalidation.

Именно эту модель интервьюер и пытается услышать в сильном ответе.

Таблица выбора: где должен жить state

СценарийLocal stateURLQuery cacheZustand
Открыт ли popoverДаНетНетРедко
Текущая страница каталогаИногдаДаНетИногда
Список товаров из APIНетНетДаНет
Выбранные строки в большой таблицеИногдаИногдаНетЧасто
Черновик формы профиляДаНетНетИногда
Глобальный режим интерфейсаРедкоИногдаНетДа
Список комментариев с refetch и retryНетНетДаНет

Эта таблица полезна не как догма, а как быстрая проверка на корректность. Если один и тот же ответ внезапно оказывается сразу в двух колонках, почти всегда рядом уже скрыт баг.

17 сложных вопросов по React data/state

1. Как определить source of truth, если одни и те же данные видны в URL, форме и таблице?

Сначала нужно разделить представление и владение. URL может хранить сериализованную форму фильтра, но не обязан быть владельцем всех derived-данных экрана. Таблица может показывать результат фильтрации, но не владеть фильтром. Форма может редактировать draft, но не быть владельцем server snapshot.

Сильный ответ обычно звучит так: источник истины определяется не тем, где данные удобно прочитать, а тем, кто имеет право считать их каноническими. Если reload страницы должен восстановить фильтр, owner, скорее всего, URL. Если данные приходят из API и могут измениться вне клиента, owner - сервер, а на фронте только query cache. Если пользователь редактирует форму до submit, owner временно draft state.

2. Когда ключ запроса спроектирован слишком грубо?

Слишком грубый key смешивает разные наборы данных и делает cache некорректным. Классический пример: и список "активных", и список "архивных" заказов кладутся под ["orders"]. Тогда refetch или mutation одного сценария случайно влияет на другой.

Слишком детальный key делает invalidation дорогой и разрывает повторное использование cache. Например, если вы включаете в key все transient-флаги интерфейса, которые не влияют на данные ответа, вы быстро получаете дробление cache и лишние запросы.

Хороший ответ должен содержать критерий: в ключ запроса включается только то, что влияет на ответ сервера.

3. Что важнее в ответе про staleTime и gcTime: определения или trade-off?

Trade-off. Определения полезны, но сами по себе мало что дают. На интервью сильнее звучит объяснение, что staleTime отвечает за агрессивность устаревания и фоновых обновлений, а gcTime - за то, как долго неиспользуемый query живет в cache. Дальше важно проговорить цену ошибки.

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

4. Когда после mutation лучше делать invalidation, а когда setQueryData?

Invalidation уместна, когда сервер может вернуть побочные изменения шире, чем знает клиент. Например, обновление заказа влияет на counters, сортировку, права доступа, derived-поля или соседние списки. Тогда безопаснее заново синхронизироваться.

setQueryData уместен, когда вы хорошо контролируете локальный эффект и хотите убрать лишний round trip. Например, вы обновляете флаг completed у одной задачи в уже загруженном списке и понимаете, что другие поля не затронуты.

Слабый ответ говорит "invalidation проще". Сильный объясняет область доверия клиенту.

5. Почему Оптимистичное обновление без отката считается неполным решением?

Потому что optimistic update - это не "быстрый UI", а временное расхождение с сервером, которое вы обязаны уметь откатить. Если запрос падает, пользователь не должен остаться в интерфейсе с ложным успешным состоянием.

На интервью полезно проговорить полный цикл: snapshot предыдущего cache, локальное применение optimistic-изменения, запрос, rollback при ошибке, финальная синхронизация при успехе или конфликте.

const updateTodo = useMutation({
  mutationFn: patchTodo,
  onMutate: async (nextTodo) => {
    await queryClient.cancelQueries({ queryKey: ["todos"] });
    const previous = queryClient.getQueryData<Todo[]>(["todos"]);

    queryClient.setQueryData<Todo[]>(["todos"], (current = []) =>
      current.map((todo) =>
        todo.id === nextTodo.id ? { ...todo, ...nextTodo } : todo
      )
    );

    return { previous };
  },
  onError: (_error, _variables, context) => {
    queryClient.setQueryData(["todos"], context?.previous);
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["todos"] });
  },
});

6. Что должно жить в URL, а что не стоит туда выносить?

В URL стоит хранить то, что должно сохраняться после перезагрузки страницы, при отправке ссылки и навигации назад: фильтры, сортировку, активный page, иногда tab. Не стоит выносить туда шумный ephemeral UI: открыта ли локальная подсказка, hover-состояние, временный selection, несохраненный draft формы.

Сильный ответ не сводит все к фразе "URL для shareable state", а добавляет цену: чем больше transient-состояния вы кладете в URL, тем сложнее синхронизация, тем больше ложных навигационных обновлений и тем хуже читаемость адреса.

7. Почему производное состояние, вычисляемое из данных запроса, часто превращается в баг спустя несколько месяцев?

Потому что команда незаметно начинает хранить вычисляемое значение как отдельное состояние. Сначала это кажется безобидным: "сохраним filteredUsers в useState, чтобы не считать каждый раз". Потом query обновляется, фильтр меняется, mutation меняет сущность, а derived state живет своей жизнью.

Если значение можно честно вычислить из query data и локальных параметров на render, отдельным источником истины оно становиться не должно.

8. Когда Zustand действительно нужен рядом с query cache?

Когда есть shared client state, который читают удаленные части дерева, и этот state не принадлежит серверу. Хорошие примеры: выбранные элементы, pinned ids, layout mode, локальный workflow мастера, состояние inspector-панели.

Плохой сценарий - использовать Zustand как "еще один cache для API". Об этом уже прямо предупреждает статья когда использовать Zustand: store и query cache решают разные задачи.

9. Почему store с селекторами масштабируется лучше, чем чтение всего объекта целиком?

Потому что селекторы уменьшают радиус обновлений. Если компонент читает весь store, любое изменение в нем потенциально делает компонент кандидатом на rerender. Если он читает только selectedId или viewMode, шум существенно ниже.

Это не только вопрос performance. Это еще и вопрос локальности мышления: компонент должен зависеть от минимального необходимого фрагмента state.

const useCatalogStore = create<CatalogState>(() => ({
  selectedId: null,
  viewMode: "table",
  isInspectorOpen: false,
}));

function InspectorToggle() {
  const isOpen = useCatalogStore((state) => state.isInspectorOpen);
  return <button aria-pressed={isOpen}>Inspector</button>;
}

10. Как проектировать cache для пагинации и infinite scroll, чтобы не ломать UX?

Нужно заранее решить, что является единицей повторного использования. Для обычной пагинации это часто комбинация filters + page. Для infinite scroll важна уже структура страниц и правило их слияния.

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

11. Что делать, если быстрые изменения фильтра и mutation начинают конфликтовать?

Это типичный race scenario. Пользователь меняет фильтр, query начинает обновляться, параллельно mutation меняет одну запись, а затем старый ответ приходит позже нового. Если data flow спроектирован слабо, экран показывает смесь эпох.

Здесь важно проговорить инструменты: отмена старых запросов, корректный query key, snapshot перед optimistic update, точечная invalidation и отказ от дублирования server state в локальном store.

12. Как брать начальные данные из сервера в форму и не перезаписывать ввод пользователя?

Нужно отделить инициализацию draft от дальнейшей жизни query data. Самая частая ошибка - держать input привязанным к server snapshot, а потом синхронизировать каждое обновление query обратно в форму. После refetch пользователь внезапно теряет свои несохраненные изменения.

Рабочий подход: один раз создать draft из server data, а дальше жить на локальной копии до submit или reset.

export function ProfileForm({ profile }: { profile: Profile }) {
  const [draft, setDraft] = useState(() => ({
    name: profile.name,
    bio: profile.bio,
  }));

  function resetFromServer() {
    setDraft({
      name: profile.name,
      bio: profile.bio,
    });
  }

  return (
    <form>
      <input
        value={draft.name}
        onChange={(e) => setDraft((s) => ({ ...s, name: e.target.value }))}
      />
      <textarea
        value={draft.bio}
        onChange={(e) => setDraft((s) => ({ ...s, bio: e.target.value }))}
      />
      <button type="button" onClick={resetFromServer}>
        Reset
      </button>
    </form>
  );
}

13. Когда нормализация данных на клиенте оправдана, а когда это лишняя сложность?

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

На интервью зрелее звучит не "нормализация всегда лучше", а "я включаю ее там, где реально есть повторное использование сущностей и сложный mutation graph".

14. Что важнее в ответе про prefetch: скорость или предсказуемость данных?

Prefetch полезен не сам по себе, а как управляемое уменьшение latency на ожидаемом переходе. Если вы blindly префетчите все подряд, вы тратите сеть, память и усложняете cache lifecycle. Если выполняете предзагрузку только для вероятных маршрутов или hover-intent сценарии, выигрыш уже осмысленный.

Здесь хорошо сослаться на связку data layer и routing: prefetch должен опираться на реальные паттерны навигации, а не на надежду, что "вдруг пригодится".

15. Как серверные push-обновления или WebSocket должны влиять на query cache?

Не стоит заводить отдельный параллельный store, который живет отдельно от query cache. Если экран уже опирается на query layer, входящие события лучше либо точечно обновляют нужный cache entry, либо помечают запрос устаревшим для последующего refetch.

Сильный ответ здесь показывает, что real-time не отменяет ownership модели. Он только добавляет еще один канал доставки изменений.

16. Почему retry и offline-режим - это не "дополнительная настройка", а часть data/state модели?

Потому что они напрямую меняют пользовательскую правду интерфейса. Если mutation не выполнилась из-за проблем с сетью, интерфейс не должен показывать, что данные уже сохранены на сервере. Если query временно недоступна, вы должны решить, что показывать: stale snapshot, ошибку, disabled actions или offline badge.

Тема особенно хорошо раскрывается там, где data layer воспринимают не как "хуки для запросов", а как часть UX-контракта.

17. Как понять, что приложение страдает не от нехватки инструментов, а от плохо определенных границ состояния?

Сигналы обычно повторяются:

  • ответ API копируется в два-три места;
  • mutation требует вручную синхронизировать несколько экранов;
  • refetch периодически ломает форму;
  • добавление нового фильтра тянет каскад правок в URL, store и компонентные useEffect;
  • команда спорит не о модели данных, а о том, "в какую библиотеку это положить".

Если вы слышите эти симптомы, проблема почти никогда не решается одной заменой Redux на Zustand или fetch на query library. Сначала нужно восстановить границы ответственности.

Прокачай React за 7 дней

20 вопросов и разборов по React Hooks

Начать

Возможные ошибки в продакшене

1. Второй источник истины появляется "временно", а остается навсегда

Команда кладет server response в store "на пару недель", чтобы быстрее сделать новую фичу. Через пару релизов этот store уже читают три экрана, mutation обновляет только половину мест, а refetch конфликтует с локальными патчами.

Признак в проде: разные части интерфейса показывают разные значения одной сущности.

2. Query key не отражает реальные параметры данных

Часть фильтров влияет на результат, но не попадает в key. В кэше оказываются несовместимые ответы, и UI ведет себя непредсказуемо после навигации или invalidate.

Признак в проде: пользователь меняет фильтр, а экран временами показывает старую выборку.

3. Draft формы живет слишком близко к server state

Пока экраны маленькие, это не видно. Но как только появляется autosave, background refetch или параллельное редактирование, пользовательские изменения начинают исчезать.

Признак в проде: "форма сама поменяла мои данные".

Разбор производительности

У data/state-тем есть и performance-цена, даже если вы не обсуждаете Profiler напрямую. Плохие boundaries почти всегда бьют по трем местам:

  • network: лишние refetch из-за неверного staleTime, key или invalidation;
  • memory: раздутый cache и дубли сущностей в нескольких слоях;
  • UI latency: широкие обновления store и лишние вычисления derived state.

Здесь полезно помнить простой trade-off. Чем агрессивнее вы кэшируете и локально патчите данные, тем меньше сеть, но выше риск сложной синхронизации. Чем чаще вы делаете полную invalidation, тем честнее состояние, но выше latency и сетевой шум. Хороший ответ не ищет единственно верный режим, а указывает на зависимость от контекста, публичный каталог, финансовый экран, совместное редактирование.

Практики, которые реально помогают

  • Разделяйте ownership данных раньше, чем выбираете библиотеку.
  • Кладите в query key только то, что влияет на ответ сервера.
  • Храните draft отдельно от server snapshot.
  • Используйте Zustand для shared client state, а не как второй query cache.
  • Делайте optimistic update только там, где готовы поддержать rollback.
  • Держите URL владельцем только shareable-состояния.
  • Вычисляйте derived state на render, если ему не нужен отдельный жизненный цикл.
  • Проверяйте архитектуру по симптомам рассинхронизации, а не только по объему кода.

Частые ошибки

  • Копировать query.data в useState без явной причины.
  • Хранить один и тот же фильтр одновременно в URL и store.
  • Считать invalidateQueries универсальным ответом для любого mutation flow.
  • Переносить server state в Zustand "для удобства селекторов".
  • Привязывать форму редактирования напрямую к данным из query cache.
  • Проектировать query key по структуре компонентов, а не по фактическим параметрам данных.
  • Говорить про state management как про выбор любимой библиотеки, а не про ownership.

Как отвечать на интервью

Хорошая формула ответа на data/state-вопрос почти всегда одна и та же:

  1. Назвать, кто владеет данными.
  2. Показать жизненный цикл: чтение, устаревание, mutation, пересинхронизация.
  3. Описать типичный production-сбой.
  4. Предложить решение и назвать его цену.

Например, если спрашивают "почему нельзя просто хранить все в Zustand?", сильный ответ не сводится к лозунгу "потому что есть React Query". Он звучит так: "Потому что у server state свой lifecycle: stale, refetch, retry, invalidation, optimistic update. Store хорошо хранит shared client state, но плохо подменяет query cache. Если смешать их роли, цена придет в виде рассинхронизации после mutation и лишней ручной синхронизации между экранами".

Подготовьтесь к React-интервью по data flow, state boundaries и server state

Практика по React state management, TanStack Query, Zustand, data fetching и архитектурным вопросам в формате реального интервью, а не набора случайных терминов

Начать подготовку

FAQ

Какие темы стоит открыть после этой статьи в первую очередь?

Если плавает база, начните со статьи client state vs server state в React. Если не хватает понимания query lifecycle, дальше логично идти в React Query под капотом. Если вопрос упирается в выбор client-store, откройте когда использовать Zustand.

Эта статья подходит junior-разработчику?

Да, но максимальную пользу она дает на границе junior и middle. Для junior здесь важно не выучить все термины, а научиться замечать ownership данных и не смешивать local state с server state.

Нужно ли наизусть знать все опции TanStack Query?

Нет. На интервью сильнее работает не перечисление API, а понимание причинно-следственной цепочки: почему key устроен так, как устроен, как mutation влияет на cache и где возникает второй источник истины.

Можно ли вообще обойтись без Zustand и React Query?

На маленьких экранах - да. Но как только появляются shared client state, background refetch, mutation, pagination и reuse данных между маршрутами, самодельные решения быстро начинают стоить дороже библиотек.

Какая ошибка чаще всего валит сильных по коду кандидатов?

Они хорошо пишут компоненты, но отвечают про data/state как про "место, куда положить данные", а не как про контракт владения, устаревания и синхронизации. На middle+ именно это и считывается как потолок.

Итоги

Сложные вопросы по React data/state почти никогда не про одну библиотеку. Они про то, можете ли вы объяснить, где живет источник истины, как он стареет, кто его обновляет и что произойдет после mutation, refetch или навигации. Если эта модель в голове собрана, и Zustand, и query cache, и URL начинают занимать свои нормальные роли.

Эту статью полезно использовать как карту вопросов перед интервью. А для углубления по отдельным темам рядом уже лежат профильные разборы: React data fetching паттерны, client state vs server state, React Query под капотом и когда использовать Zustand.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью

Подписаться

Автор

Lexicon Team

Читайте также