Irbisa · Шпаргалка

Загрузка данных

Запросы в эффектах и их ловушки, TanStack Query, кэш и инвалидация, Suspense для данных, пагинация, оптимистичные обновления, ошибки

Middle-разработчик · 21 вопрос

  1. 01

    Как загрузить данные из API в React-компоненте через useEffect и какие состояния нужно обработать?

    Запустите запрос в useEffect, указав в массиве зависимостей входные данные запроса, сохраните результат в стейт и отрисуйте три состояния: загрузку, ошибку и данные.

    type User = { id: string; name: string }; function Profile({ userId }: { userId: string }) {  const [user, setUser] = useState<User | null>(null);  const [error, setError] = useState<Error | null>(null);…
  2. 02

    Что такое TanStack Query и какие проблемы он решает по сравнению с загрузкой в useEffect?

    TanStack Query (раньше React Query) представляет собой кэш для серверного состояния: вы описываете запрос через queryKey и queryFn, а он берёт на себя состояния загрузки и ошибки, кэширование, дедупликацию запросов, фоновый перезапрос, повторные попытки и инвалидацию после мутаций.

    import { useQuery } from '@tanstack/react-query'; type User = { id: number; name: string }; async function fetchUser(id: number): Promise<User> {  const res = await fetch(`/api/users/${id}`);…
  3. 03

    Axios или fetch: в чём практическая разница и почему fetch не отклоняет промис при ответе 404 или 500?

    fetch отклоняет промис, только когда ответ не пришёл совсем (сбой сети, блокировка CORS, abort); 404 или 500 всё равно являются корректным HTTP-ответом, поэтому промис выполняется с res.ok === false.

    // fetch: resolves for any HTTP status, rejects only when no response arrivesasync function getUser(id: string): Promise<User> {  const res = await fetch(`/api/users/${id}`);  if (!res.ok) throw new Error(`HTTP ${res.status}`); // 404 and 500 end up here  return (await res.json()) as User;}…
  4. 04

    Какие подводные камни у загрузки данных прямо в useEffect?

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

  5. 05

    Как проектировать query keys в TanStack Query и почему каждая переменная, которую использует запрос, должна быть в ключе?

    Query key служит адресом в кэше: одинаковый ключ означает одни и те же закэшированные данные.

    import { queryOptions } from '@tanstack/react-query'; type Filters = { status: 'open' | 'done'; page: number }; export const todoQueries = {  all: () => ['todos'] as const,…
  6. 06

    Найдите баг: по нажатию «Next page» номер страницы меняется, но список продолжает показывать первую страницу.

    function Orders() {  const [page, setPage] = useState(1);  const { data } = useQuery({    queryKey: ['orders'],    queryFn: () => fetchOrders(page),  });   return (    <>      <OrderList orders={data?.items ?? []} />      <button onClick={() => setPage((p) => p + 1)}>Next page ({page})</button>    </>  );}

    page используется в queryFn, но отсутствует в queryKey, поэтому запись в кэше одна для всех страниц: TanStack Query видит неизменный ключ, оставляет закэшированную первую страницу и не делает перезапрос.

  7. 07

    Чем отличаются staleTime и gcTime в TanStack Query и какие у них значения по умолчанию?

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

    import { QueryClient } from '@tanstack/react-query'; export const queryClient = new QueryClient({  defaultOptions: {    queries: {      staleTime: 60_000, // fresh for a minute: no refetch on mount or focus…
  8. 08

    Чем в TanStack Query v5 отличаются флаги isPending, isFetching и isLoading и когда какой использовать?

    isPending означает, что данных ещё нет (status === 'pending'); isFetching означает, что запрос выполняется прямо сейчас, включая фоновые перезапросы уже показанных данных; isLoading означает и то и другое сразу, то есть самую первую загрузку.

  9. 09

    Когда TanStack Query автоматически перезапрашивает данные и как этим управлять?

    Устаревший запрос перезапрашивается в фоне, когда монтируется новый компонент, который его использует, когда окно снова получает фокус и когда восстанавливается сеть; с refetchInterval он ещё и опрашивает сервер.

  10. 10

    Как в TanStack Query заставить один запрос дождаться другого (зависимые запросы)?

    Передайте результат первого запроса в ключ второго и выключите второй, пока этого значения нет: через enabled: !!userId или, с сохранением типов, `queryFn: userId ?

    import { skipToken, useQuery } from '@tanstack/react-query'; function Projects({ email }: { email: string }) {  const { data: user } = useQuery({    queryKey: ['user', email],    queryFn: () => getUserByEmail(email),…
  11. 11

    Как изменять данные через useMutation и как после этого держать закэшированные списки в актуальном состоянии?

    useMutation({ mutationFn }) выполняет запись и отслеживает её isPending, error и data; сам по себе он кэш не трогает.

    import { useMutation, useQueryClient } from '@tanstack/react-query'; function AddTodo() {  const queryClient = useQueryClient();  const addTodo = useMutation({    mutationFn: (title: string) => api.createTodo(title),…
  12. 12

    Найдите баг: после добавления новой задачи список не показывает её, пока страницу не перезагрузят.

    function ProjectTodos() {  const { projectId = '' } = useParams(); // "42", a string from the URL  const queryClient = useQueryClient();  const { data: todos = [] } = useQuery({    queryKey: ['projects', projectId, 'todos'],    queryFn: () => api.getTodos(projectId),    staleTime: 5 * 60_000,  });  const addTodo = useMutation({    mutationFn: api.createTodo, // resolves with the saved todo, projectId: 42    onSuccess: (todo) =>      queryClient.invalidateQueries({ queryKey: ['projects', todo.projectId, 'todos'] }),  });  return <TodoList todos={todos} onAdd={(title) => addTodo.mutate({ projectId, title })} />;}

    Мутация инвалидирует ['projects', 42, 'todos'] с числом из ответа сервера, а список закэширован под ['projects', '42', 'todos'] со строкой из URL.

  13. 13

    Как сделать оптимистичное обновление в TanStack Query, включая откат при ошибке?

    В onMutate отмените текущие запросы по этому ключу, сохраните снимок кэша, запишите ожидаемый результат через setQueryData и верните снимок; в onError положите снимок обратно; в onSettled сделайте инвалидацию, чтобы в любом случае кэш в итоге совпал с сервером.

    type Todo = { id: number; title: string; done: boolean }; function useToggleTodo() {  const queryClient = useQueryClient();  return useMutation({    mutationFn: (todo: Todo) => api.updateTodo({ ...todo, done: !todo.done }),…
  14. 14

    Как реализовать бесконечный скролл с TanStack Query?

    Используйте useInfiniteQuery: задайте initialPageParam, queryFn, который читает pageParam, и getNextPageParam, который возвращает следующий курсор из последней страницы или undefined, если страниц больше нет.

    import { useInfiniteQuery } from '@tanstack/react-query'; type Page = { items: Post[]; nextCursor: string | null }; function Feed() {  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({…
  15. 15

    Как обрабатывать ошибки при загрузке данных в React: повторные попытки, error boundaries и ошибки фонового обновления?

    Для каждой ошибки решите, где ей место: неудачная первая загрузка заменяет контент (сообщение на месте или error boundary), неудачный фоновый перезапрос оставляет старые данные с небольшим уведомлением, а неудачная мутация показывается рядом с действием.

  16. 16

    Как отменить запрос, который больше не нужен, например когда пользователь ушёл со страницы или ввёл новый поисковый запрос?

    Передайте AbortSignal в fetch и вызовите abort, когда результат больше не нужен.

    function SearchResults({ query }: { query: string }) {  const [results, setResults] = useState<Hit[]>([]);   useEffect(() => {    const controller = new AbortController();    fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })…
  17. 17

    Как обновить истёкший access-токен, если несколько запросов одновременно получили 401?

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

    let refreshing: Promise<string> | null = null; function refreshAccessToken(): Promise<string> {  // every 401 that arrives during a refresh waits for this same promise  refreshing ??= fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' })    .then(async (res) => {…
  18. 18

    Как в React 19 работает Suspense для загрузки данных и как использовать его с TanStack Query?

    Компонент, которому нужны ещё не загруженные данные, приостанавливается (suspend), и ближайший <Suspense> показывает свой fallback, пока данные не придут; сам компонент рендерит только успешный случай.

    import { Suspense } from 'react';import { useSuspenseQuery } from '@tanstack/react-query';import { ErrorBoundary } from 'react-error-boundary'; function UserName({ id }: { id: number }) {  const { data } = useSuspenseQuery({…
  19. 19

    Найдите баг: компонент навсегда застревает на fallback от Suspense, а во вкладке Network на каждый рендер появляется новый запрос.

    import { Suspense, use } from 'react'; function UserCard({ id }: { id: number }) {  const user = use(fetch(`/api/users/${id}`).then((r) => r.json()));  return <p>{user.name}</p>;} export function Page({ id }: { id: number }) {  return (    <Suspense fallback={<p>Loading...</p>}>      <UserCard id={id} />    </Suspense>  );}

    Промис создаётся во время рендера, поэтому каждый рендер передаёт в use совершенно новый промис: React приостанавливается, запрос завершается, React рендерит заново, стартует новый fetch, и компонент снова приостанавливается.

  20. 20

    Что такое водопад запросов (request waterfall) в React-приложении и как его избежать?

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

  21. 21

    Как предзагрузить данные на сервере и гидратировать их в TanStack Query на клиенте (SSR, Next.js App Router)?

    Создайте QueryClient на каждый запрос на сервере, выполните await queryClient.prefetchQuery(...) для того, что нужно странице, и передайте dehydrate(queryClient) в <HydrationBoundary>; клиентские компоненты ниже вызывают тот же useQuery и находят данные уже в кэше, без состояния загрузки и без второго запроса.

    // app/posts/page.tsx (Server Component)import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query';import { postsQuery } from './queries';import { PostList } from './PostList'; // 'use client', calls useQuery(postsQuery) export default async function PostsPage() {…