Gernar
Frontend DeveloperReact, Redux и API

Интервью-вопрос

Что такое RTK Query

RTK Query - это слой Redux Toolkit для серверных данных: он описывает API, выполняет запросы, кэширует ответы и дает готовые хуки для компонентов. Главный риск в ответе - назвать его просто fetch-оберткой и забыть про кэш, инвалидацию и границы ответственности.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 60 правильно

Как лучше коротко объяснить RTK Query на интервью?

Вы отвечаете на базовый вопрос и хотите сразу показать практический смысл.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

RTK Query нужен не просто для вызова fetch. Он помогает решить более важную задачу: как во frontend-приложении хранить и обновлять данные, которые пришли с сервера.

Без RTK Query для запроса часто приходится писать action types, thunk, reducer, поля loading и error, ручную очистку и повторную загрузку. В RTK Query вы переносите эту рутину в декларативное описание API. Вы описываете endpoint на backend, а библиотека создает action, reducer logic, middleware integration и React-хуки.

На интервью можно сказать коротко:

RTK Query - это встроенный в Redux Toolkit инструмент для server state. Он выполняет запросы, кэширует ответы, отслеживает подписки компонентов, дает статусы загрузки и помогает обновлять кэш после мутаций.

Как это выглядит в коде

Минимальный пример показывает главный контракт: createApi описывает базовый запрос, endpoints и типы данных. React-хуки генерируются из имен endpoints.

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

type Post = {
  id: string;
  title: string;
};

export const postsApi = createApi({
  reducerPath: "postsApi",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  tagTypes: ["Post"],
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => "posts",
      providesTags: ["Post"],
    }),
    addPost: builder.mutation<Post, Pick<Post, "title">>({
      query: (body) => ({
        url: "posts",
        method: "POST",
        body,
      }),
      invalidatesTags: ["Post"],
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = postsApi;

В компоненте вы работаете не с ручным useEffect, а с hook, который уже связан с кэшем:

function PostsList() {
  const { data: posts = [], isLoading, isFetching, error } = useGetPostsQuery();
  const [addPost, { isLoading: isAdding, error: addError }] = useAddPostMutation();

  async function handleAddPost() {
    try {
      await addPost({ title: "New post" }).unwrap();
    } catch {
      // Show a user-friendly message near the action or keep it in mutation state.
    }
  }

  if (isLoading) return <p>Загружаем посты...</p>;
  if (error) return <p role="alert">Не удалось загрузить посты</p>;

  return (
    <section aria-busy={isFetching || isAdding}>
      {isFetching && <p>Обновляем данные...</p>}
      {posts.length === 0 ? (
        <p>Пока нет постов</p>
      ) : (
        <ul>
          {posts.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      )}

      {addError && <p role="alert">Не удалось добавить пост</p>}
      <button type="button" disabled={isAdding} onClick={handleAddPost}>
        {isAdding ? "Добавляем..." : "Добавить пост"}
      </button>
    </section>
  );
}

Практический вывод простой. Компонент становится короче, но ответственность не исчезает. Вам все равно нужно решить, когда refetch уместен, какие ошибки показать пользователю и как не оставить в кэше старый список. В UI важно не только получить данные. Нужно еще показать первую загрузку, empty state, фоновое обновление и ошибку так, чтобы пользователь не потерял контекст. Если просто вызвать mutation и не обработать отказ сервера, действие может выглядеть успешным, хотя данные не сохранились.

Когда выбирать RTK Query

На интервью не стоит продавать RTK Query как универсальное решение для всего. Лучше назвать критерии выбора. Инструмент хорошо подходит, когда в проекте уже есть Redux Toolkit, REST-запросов много, данные нужны в разных частях приложения, а ручные slices для каждого запроса превращаются в шум.

Если задача маленькая, данные нужны в одном компоненте или команда уже использует другой зрелый инструмент для server state, выбор может быть другим. Ответ звучит увереннее, когда вы показываете trade-off, а не доказываете, что один инструмент всегда лучше.

Как выбрать место для состояния

1Данные приходят с сервера и используются в нескольких местах?
Описать endpoint в RTK Query и читать данные из кэша через hook.
2Это локальный UI-флаг, выбранная вкладка или состояние формы?
Оставить в component state, form library или обычном Redux slice.
3После действия меняется список или карточка на сервере?
Добавить invalidatesTags или точечно обновить кэш.
4Нужен мгновенный UI до ответа сервера?
Использовать optimistic update только с rollback на ошибке.

Кэш и инвалидация

Главная ловушка RTK Query в том, что кэш кажется полностью автоматическим. На деле он умеет хранить ответы и переиспользовать их, но бизнес-связь между запросами вы обычно описываете сами.

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

Что важно настроить вокруг кэша

QueryprovidesTags

Помогает понять, какие данные в кэше относятся к списку, карточке или сущности.

MutationinvalidatesTags

После изменения на сервере обновляет активные query и снижает риск старого UI.

Optimistic UIonQueryStarted

Дает быстрый отклик, но требует отката, если сервер вернул ошибку.

Повторный запросrefetchOnFocus

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

Server state не равен client state

RTK Query хранит копию серверных данных и управляет ее жизненным циклом. Это не значит, что все состояние приложения нужно переносить в API-кэш.

Хорошее разделение выглядит так:

  • данные пользователя, список заказов, карточка товара - query cache;
  • открыта ли модалка, выбранная вкладка, текст в поисковом поле - component state или обычный slice;
  • черновик формы редактирования - form state или локальный state, пока пользователь не отправил mutation.

Плохой паттерн - без причины копировать data из query в useState и дальше показывать копию. При refetch кэш обновится, а локальная копия может остаться старой. Безопаснее читать серверные данные из hook, а отдельно хранить только то, что пользователь реально редактирует.

Оптимистичные обновления

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

Поэтому в ответе стоит сказать не только, что RTK Query поддерживает optimistic updates. Важно добавить, что нужен rollback. В RTK Query для этого используют onQueryStarted и обновление кэша через utility methods. Если mutation падает, patch нужно откатить.

removePost: builder.mutation<void, string>({
  query: (id) => ({
    url: `posts/${id}`,
    method: "DELETE",
  }),
  async onQueryStarted(id, { dispatch, queryFulfilled }) {
    const patch = dispatch(
      postsApi.util.updateQueryData("getPosts", undefined, (draft) => {
        return draft.filter((post) => post.id !== id);
      })
    );

    try {
      await queryFulfilled;
    } catch {
      patch.undo();
    }
  },
})

Такой пример показывает зрелость ответа. Вы думаете не только о скорости, но и о консистентности данных и доверии пользователя.

Что сказать про TypeScript

RTK Query хорошо сочетается с TypeScript, потому что типы endpoint описывают и данные ответа, и аргумент запроса. В примере builder.query<Post[], void> означает, что hook вернет массив Post, а аргумент ему не нужен. builder.mutation<Post, Pick<Post, "title">> означает, что mutation принимает объект с title и возвращает созданный post.

Это не магия и не гарантия правильного runtime-ответа от backend. TypeScript помогает не передать неправильный аргумент и не обратиться к несуществующему полю. Но если сервер вернул неожиданный JSON, нужна валидация на границе или аккуратная обработка ошибки.

Безопасность и внешние данные

RTK Query не делает данные безопасными автоматически. Не рендерите HTML из ответа без санитайза, не кладите токены в query args и не отправляйте credential-запросы без понимания CSRF-защиты на backend. Для авторизации обычно используют общий слой вроде prepareHeaders, а не пробрасывают секреты из компонента в каждый hook.

Практический вывод

Сильный ответ на вопрос, что такое RTK Query, можно построить так:

  1. Назвать инструмент: часть Redux Toolkit для server state.
  2. Объяснить, что он делает: запросы, кэш, статусы, подписки, хуки.
  3. Показать пользу: меньше boilerplate, меньше ручного useEffect и reducers для API.
  4. Добавить важный нюанс: после мутаций нужно думать про invalidation или ручное обновление кэша.
  5. Провести границу: не заменяет все состояние приложения и не отменяет обработку ошибок.

Если ответить только определением, это прозвучит поверхностно. Если добавить кэш, инвалидацию, server state и пару рисков для UI, ответ будет похож на ваш опыт работы с инструментом, а не на пересказ документации.

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

Где обычно ошибаются

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

  1. 1

    Называть RTK Query заменой всего Redux

    RTK Query закрывает серверное состояние, но не отменяет обычные slices для клиентского состояния. Если хранить все подряд в API-кэше, код становится неясным, а UI-данные начинают зависеть от lifecycle запросов.
  2. 2

    Забывать про инвалидацию после мутаций

    После POST, PATCH или DELETE старый список может остаться в кэше. Без invalidatesTags пользователь увидит устаревшие данные и может повторить уже выполненное действие.
  3. 3

    Дублировать данные из кэша в локальный state

    Копия через useState часто расходится с актуальным кэшем и ломает refetch. Лучше читать data из hook, а локально хранить только редактируемый черновик или UI-состояние.
  4. 4

    Делать optimistic update без rollback

    Если запрос упал, а интерфейс уже изменился, пользователь получает ложное подтверждение. В onQueryStarted нужно сохранить patch result и откатить его через undo при ошибке.
  5. 5

    Считать isLoading и isFetching одним и тем же

    isLoading обычно важен для первого запроса без данных, а isFetching показывает фоновое обновление. Если всегда скрывать экран на isFetching, пользователь будет видеть лишние мерцания вместо стабильного UI.
  6. 6

    Передавать секреты через query args

    Аргументы endpoint и кэш RTK Query находятся в Redux state и могут попасть в DevTools, логи или persisted state. Не передавайте токены, пароли и одноразовые коды как аргументы hook. Безопаснее добавлять авторизационный заголовок в prepareHeaders или другой общий слой, который не раскрывает секрет в ключе кэша.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми интервьюер проверяет понимание RTK Query, кэша и server state.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

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

Содержание