Галоўная / Артыкулы / TanStack Query для React: кэшаванне, паўторная загрузка і мутацыі

TanStack Query для React: кэшаванне, паўторная загрузка і мутацыі

Заменіце стандартны код useEffect fetch на TanStack Query: ключы запитоў, параметры staleTime і gcTime, мутаціі, а таксама супакойцеся, калі бібліятэка не патрэбна.

1840 слоў

Як кэшуецца, апдэюецца і падтрымліваецца стан асінхроннага сервера — і калі варта дадзіць такую бібліятэку.

Большасць дапраў на React пачынаюцца з аднаковага спосабу загрузкі дадзэнняў: useEffect, fetch і калькі флагаў useState для паказу стану «чакаецца» і адзінакоў. Гэты патэрн падходы для першага варыянта. Адночасова самэ тут часта з’яўляюцца дублікатныя запыты, застарэлыя экраны і копіюваны-выкладзены шаблонны код.

У наступных раздзелах рассматрываюцца гэтыя проблемы і паказваецца, як TanStack Query іх рашае. Едынственныя прыямыя патрабаванні — гакі React і базовы TypeScript. У прыкладах викорыстоўваецца DummyJSON, публічны API, які не выкарыстоўвае ключ API.

1. Базовы патэрн

Спіс продуктав, напісаны з ужываннем стандартных гакоў, выглядае так.

import { useEffect, useState } from "react";
type Product = {
  id: number;
  title: string;
  price: number;
};
function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    let cancelled = false;
    setIsLoading(true);
    fetch("https://dummyjson.com/products?limit=10")
      .then((res) => {
        if (!res.ok) throw new Error("Request failed");
        return res.json();
      })
      .then((data: { products: Product[] }) => {
        if (!cancelled) setProducts(data.products);
      })
      .catch((err: Error) => {
        if (!cancelled) setError(err.message);
      })
      .finally(() => {
        if (!cancelled) setIsLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, []);
  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>{error}</p>;
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.title} - ${product.price}
        </li>
      ))}
    </ul>
  );
}

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

2. Чаго не адбірае гэты код

Сама функцыя fetch працюе нормальна. Проблемы з’яўляюцца ў суседзічных частках коду.

  • Няма кэша. Як толькі завершыта робота з маршрутом, сеанс заканчваецца, і сетавы запит выконвуецца знова, нават якщо даны не зменіліся.
  • Няма усунення дублікацый запытав. Тры компаненты, якім патрэбны тыя ж даны пра продукты, выкананяюць тры ідэнтычныя запыты.
  • Няма прабавакоў. Адна перерваная з’язок стае прычыной паказу адмовы, нават калі другая спроба могла бы успець.
  • Няма паўтарной пераверкі. Картака, якая застаецца ачытай гадзіну, продовжвае паказваць старыя даны, пакуль штое не спрычыніць ўзяць іх знову.
  • Условы гонкі. Калі параметр вводу, такі як запитаўны тэрмін, зменшыцца быстра, старэйшая адпаведна адпаведь можа перазапісаць новэйшую. Флаг cancelled дапамагае пад час завершэння задачі; але ён не вырашае пачоўна проблему перакрыцча запускаемых запитоў.
  • Павтарэнне стандартных частак коду. Той самы код для завантажэння, адобразавання памылак і чысткі ўскладнень дублюецца ў кожным компоненте для завантажэння дадзеных.
  • Кожная з гэтых проблем мае вядомы спосаб вырашэння. Рэалізацыя гэтых спосабоў вручную значыць стварэнне саслова кешавання самостайна.

    3. Ідея, стоячая за бібліятэкай

    Корыстны ўпадзел — на два відэры стану.

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

    Стан сервера ўзьяты напрацо. Ён знаходзіцца ў сховішчы, якое вы не кантролюеце. Іншыя корыстувальнікі можаць яго зменіць, а копія ў браузеры є толькі момантальным знімкам. Зберагчы гэты знімак толькі ў useState, вы прыемляеце, што тэмпаратарны відблік є автаномным.

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

    Холадзік ўпоряднае аналагія. Малака застаецца ў доме, таму вам не трэба хадзіць у магазін за кожным стаканчыкам кавы, але ёна выгоршваецца, таму вы пераканаліваеце дату і падкладзіце яе занова, перш чым ёна зіпсуецца. TanStack Query выпалюе гэту ролю для адпаведзей API.

    4. Чаму ёсць TanStack Query

    TanStack Query кантролюе асінхронныя удаленныя станы ў додатках у браузеры. Проект распространяецца пад ліцензыей MIT, яго можна безкоштовна вжываць, і ён падазроўваецца ў большасці проектаў на React.

    Адукацыйныя матэрыялы, напісаныя летамі раней, яшчэ згадваюць React Query. Гэты назва застаўся да версіі 3. У версіі 4 проект быў перайменаваны на TanStack Query пасля таго, як былі выпусканы адаптары для Vue, Svelte, Solid і Angular. У React вы яшчэ інсталюеце @tanstack/react-query; чырвоная версія — 5.

    Ён не ўзаменяе fetch чы або axios. Функцыя запита застаецца вашай. Навакол гэтай функцыі бібліятэка керуе часам выканання, зберагаччуком, свежасцю дадзеных і адрабатаваннем адказоў пра бяды.

    5. Наладжэнне

    Для пачатку достатня два крокі.

    npm install @tanstack/react-query
    

    Потым абгорніце весь структурны дрэво ў коране:

    // main.tsx
    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    import App from "./App";
    
    const queryClient = new QueryClient();
    
    export default function Root() {
      return (
        <QueryClientProvider client={queryClient}>
          <App />
        </QueryClientProvider>
      );
    }
    

    QueryClient — это экзанпляр кэша. QueryClientProvider робіць яго доступным для кожнага наступнага компонента.

    6. Той самы компонент, перапісаны

    import { useQuery } from "@tanstack/react-query";
    
    type Product = {
      id: number;
      title: string;
      price: number;
    };
    
    async function fetchProducts(): Promise<Product[]> {
    
      const res = await fetch("https://dummyjson.com/products?limit=10");
    
      if (!res.ok) throw new Error("Request failed");
      const data: { products: Product[] } = await res.json();
      return data.products;
    }
    
    function ProductList() {
    
      const { data, isPending, isError, error } = useQuery({
        queryKey: ["products"],
        queryFn: fetchProducts,
      });
    
      if (isPending) return <p>Loading…</p>;
      if (isError) return <p>{error.message}</p>;
      return (
        <ul>
          {data.map((product) => (
            <li key={product.id}>
              {product.title} - ${product.price}
            </li>
          ))}
        </ul>
      );
    }
    

    Прыгае з прыблізна чатырохдзесят ліній да апошней часоў пятнаццаць. Значэнне data задаецца як Product[] без дадатковых анотацый, калі тып выводзіцца з fetchProducts. Пасля розгалужэнняў isPending і isError TypeScript сцанавляе, што data ўжо визначана, таму няма неабавязковага запэўнення для map і няма асерцыі пра тое, што значэнне не ў значэнні null.

    7. Што дае болей кораткая версія

    Параўняўшы з праслойкамі з раздзела 2, стандартныя значэння вже пакрываюць частыя кейсы:

    • Remount адразу паказвае запам’ятаваны данні і перапрацоўвае іх у фонавым режыме.
    • Ідэнтычныя запиты, якія ў выпалоўцы, злучаюцца ў адзін запит.
    • Неудачныя запыты прабуюцься зноў автаматычна (за значэннем тры спробы, з павышэнням інтэвалу).
    • Старыя элементы прабуюцься зноў, калі акцэнс на вікнах, восстанавліваецца з’ўязак сеті або праканчваецца перезапуск.
  • У кэшы зберагаецца толькі найсвежэйшы адказ на паведамленне, што скаржае рызыкі, вытэчаючыя з абсалютнага адначасовасці.
  • Одна вузелка заменяе ручныя запускі та станы адзінакоў.
  • Ніякі з гэтых параметраў не быў насталяваны ў перапісанай складовай. Гэта стандартная настаўка бібліятэкі.

    8. Тры аспекты, якія варта зразумець

    Большая частка пачатковых плутанняў выходзіць з наступных трох ідэй.

    Ключ запыту

    queryKey — гэта адрес кэшу. Две складовыя, якія абодва вжываюць ["products"], дзеляцца адной ентрыю та аднам сеансам з’яўлення ў сеті.

    Практычная правіла: кожнае значэнне, ад якога залежыць функцыя запыту, павінна быць прыключанае да ключа.

    function ProductList({ category }: { category: string }) {
      const { data } = useQuery({
        queryKey: ["products", category],
        queryFn: () => fetchProductsByCategory(category),
      });
      // …
    }
    

    Якщо з ключа не выключыць category, змена катэгоріі все равно можа паказаць кэшаваны список пярэдней катэгоріі. Гэта частая памылка у першыя разы користуецца.

    staleTime і gcTime

    Іх назвы супадаюць, але яны означаюць разнае.

    staleTime задае періад свежасці. У межах гэтаго періаду біблія прыпіняе роботу з сетью. За стандартным значэнням 0 рэзультат негаразда ўжо з самага пачатку: інтэрфейс все ўжо можа адобразіць зберажаны значэнне, пры тым як будь-яка дзія запускае фонавую апдэйт. Паднесіце значэнне, калі даны рэдкая змінююцца:

    useQuery({
      queryKey: ["products"],
      queryFn: fetchProducts,
      staleTime: 5 * 60 * 1000, // fresh for five minutes
    });
    

    gcTime — гэта час, пры якім невыкарыстоўваныя даны застаюцца ў памяці пасля таго, як апошній корыстувальнік завершыў роботу. Стандартнае значэння — пяць хвілін. Калі гэты періад заканчываецца, энтрыя выкарыстоўванае і наступны запуск ведаецца з нуля.

    Корача кажучы: staleTime керуе практыкай паўтарнага запрашэння даных; gcTime керуе ўдаленнем даных.

    Калі вядзецца паўтарная загрузка

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

    useQuery({
      queryKey: ["products"],
      queryFn: fetchProducts,
      refetchOnWindowFocus: false,
    });
    

    Функцыя перзапрашывання пад актуальнасцю вікна дзівуе людей праз першы раз, калі яны ўбачаюць яе. Зазвычай самэўключэння гэтой функцыі ёсць прычыной таго, што вкладка, якая дзейнае даволі дазго, застаецца актуальной.

    9. Змена дадзэнняў за дапамою useMutation

    useQuery выкарыстоўваецца для чытання. useMutation — для запісу.

    import { useMutation, useQueryClient } from "@tanstack/react-query";
    
    type NewProduct = {
      title: string;
      price: number;
    };
    
    function AddProductButton() {
    
      const queryClient = useQueryClient();
    
      const { mutate, isPending } = useMutation({
    
        mutationFn: async (product: NewProduct) => {
          const res = await fetch("https://dummyjson.com/products/add", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(product),
          });
    
          if (!res.ok) throw new Error("Could not add product");
          return res.json();
        },
    
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: ["products"] });
        },
      });
    
      return (
        <button
          onClick={() => mutate({ title: "New product", price: 25 })}
          disabled={isPending}
        >
          {isPending ? "Saving…" : "Add product"}
        </button>
      );
    }
    

    Важным є элемент invalidateQueries. Гэты вызов пазначае кожны элемент кешу пад прыметнікам ["products"] як застарэлы, таму спостерагачы, якія запускаюцца, негараздо прасіюць свежыя дадзеныя. Локальныя масівы не прадаюцца вручную, і сторанце не трэба павольная перзагрузкі.

    10. Адміністрацыйныя інструменты

    npm install @tanstack/react-query-devtools
    
    import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
    
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
    

    У стадії розработкі панель паказвае кожны ключ запиту, статус, даныя, які перадаюцца, і час последнага запыту. Адзірванне, як элементы перайшлі з категоріі „свежыя“ у категорію „старыя“ праз кліканне, дапамагае шырэй за простае чытанне зразумець модель кэшу. Пакет автаматычна адключаецца з версій, прызначаных для практычнага вжытку.

    Пашыльныя памылкі

    • Выключэнне зменных з ключа запиту. Якщо функцыя запиту чытае якое-небудзь значэнне, гэты ключ павінен яго уключыць.
    • Выкарыстоўванне useQuery унутрань useEffect. Гакі хук вже выкананы пад час атрыбутавання элемента; няма нічога дадатковага, што трэба было бы „запускаць“.
    • Выкарыстоўванне бібліятэкі толькі для стану кліента. Поле формы і флагі модалных вікна павінны быць у useState.
    • Установка staleTime: Infinity у всіх месцах. Гэта выключае павтапераканранне данных, чым усунуцца большая частка прынадоў бібліятэкі.
  • Копіюванне data у локальны стан. Тады вы падтримваеце два копіі, а тая, якая відрасліваецца, перестае стежыць за кэшам.
  • 12. Калі гэта можа не быць неабяжным

    Якщо прыложэнне выкарыстоўвае адзін канцэнтрабы на адной экране, то викорыстоўванне прадаўцоў і хуківа можа быць занадта сложным для рашэння проблемы.

    Якщо фреймворк вже забезпечвае шар дадзеных — компаненты сервера Next.js або маршрутаўзер з ладэрамі — частка работы вже адбылася. TanStack Query все ж можа дапамагчы для інтэрактыўных запытаў з боку кліента, але гэта не ўмовна.

    Для стану, які ніколі не выходзіць за межы браузера, выберыце інструмент другі.

    13. Куды працаваць далей

    Падставныя задачы пакрываюцца вышэўказанымі концэптамі. Глыбокія тэмы знаходзяцца ў іншых месцах:

    • Афіцыйная дакументацыя — матэрыялы для апылення і інтэрактыўныя дамэ
    • Спісакі з сторанаваннем і безканальныя спісакі — для загрузкі большае колькасць сторанак ужывайте useInfiniteQuery
    • Запыткі, які чакаюць на іншыя запыткі — блакуйце дадзеннія пасля адной запыткі, калі enabled ўвімкнуты, пакуль не завершыцца папярэдні запыт
    • Оптымістычны інтерфейс — адразу паказвайце бажаны рэзультат, прычаму адпаведная вясна з’явіцца пасля завершэння мутацыі

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