Jak funguje TanStack Query

U webové aplikace napojené na API dokáže TanStack Query (dříve React Query) výrazně zpříjemnit uživatelský i vývojářský zážitek.

Jde použít jako mezivrstvu mezi zobrazením dat a stávajícím řešením pro jejich stahování – ať už se používá obyčejný fetch, knihovna typu Axios nebo třeba nějaký GraphQL klient.

Invalidace dat

Asi hlavní výhoda je invalidace starých dat.

Když u složitější aplikace uživatel něco změní, může být problém to promítnout na všechna místa.

TanStack Query funguje tak, že se každé specifické volání označí unikátním klíčem (queryKey):

const query = useQuery({
  queryKey: ['todos'],
  queryFn: getTodos
})

Do queryFn se nastaví funkce pro získání dat, která vrací promisu.

V návaznosti na nějakou akci (změnu dat) se následně volá invalidace:

const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: ['todos'] })

Vývojář tak vůbec nemusí řešit, kde se co má obnovit, ale TanStack Query to vyřeší.

Cache

Zároveň jsou data ukládána do cache. To znamená, že odpadá nějaké složité předávání dat napříč komponentami, aby se šetřily HTTP requesty.

Každá komponenta si klidně může sahat znovu a znovu pro data, ale TanStack Query zajistí, že se nic zbytečně stahovat nebude.

Zde je užitečné si vytvářet znovupoužitelné funkce, např.

export function getTodosQueryKey() {
  return ['todos'] 
}

export function useTodosQuery() {
  return useQuery({
    queryKey: getTodosQueryKey(),
    queryFn: getTodos
  })
}

Na potřebném místě tak stačí jen:

const query = useTodosQuery()

A pro invalidaci něco jako:

const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: getTodosQueryKey() })

A je krásně vyřešena konsistence napříč celou aplikací.

Invalidace cache

Kromě ruční invalidace se ve výchozím stavu obnovují data i při znovuaktivování okna prohlížeče a obnovení internetového připojení.

Je možné si i nastavit čas, po kterém se mají data automaticky obnovit, pouhým nastavením vlastnosti refetchInterval.

TanStack Query se zde chová navíc chytře a porovnává v JSON datech, jestli došlo k nějaké změně. A pokud ne, tak se nic nedělá. Obnova dat tak ve většině případů nemá viditelný negativní dopad na uživatele.

Znázornění stavů

Aplikace potřebuje znázorňovat stav načítání.

To se řeší tak, že v query objektu existují vlastnosti data, isPending, isSuccess, isError a další:

if (query.isPending) {
  return <span>Načítání</span>
}

if (query.isError) {
  return <span>Chyba: {error.message}</span>
}

return (
  <ul>
    {query.data.map((todo) => (
      <li key={todo.id}>{todo.title}</li>
    ))}
  </ul>
)

Ruční úprava dat

Hodně zajímavá je možnost nastavit pro daný queryKey data ručně.

To se typicky může hodit třeba při přechodu z výpisu na detail položky, kde je většina dat z detailu stažena už ve výpisu.

Takže může být zajímavé tato data přes initialData nastavit jako základ:

const result = useQuery({
  queryKey: ['todo', todoId],
  queryFn: () => fetch(`/todos/${todoId}`),
  initialData: () => {
    return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId)
  },
})

TanStack Query stáhne i opravdová data detailu, ale uživatel do té doby může vidět alespoň ten základ z výpisu a aplikace tak reaguje okamžitě.

Kromě nastavení výchozích dat jde ručně nastavovat i skutečná data daného klíče:

queryClient.setQueryData(['todo', { id: variables.id }], data)

To jde rozumně využít například u endpointů pro ukládání dat. Pokud takové volání skončí úspěchem, mohou se tato data rovnou nastavit do potřebného query klíče, aniž by se musel znovu volat endpoint pro čtení dat.

Optimistické updaty

Pokročilejší technika jsou optimistické updaty. O co jde? Aplikace spoléhá na to, že se akci povede dokončit. Jsou-li dobře vyřešené validace na straně klienta, není moc reálně důvod, že by volání API mělo skončit chybou.

Takže se ihned po vyvolání akce uživatelem nastaví do query klíčů očekávaná data. Zároveň se zavolá API pro danou akci. A mohou nastat dva případy:

  1. Akce skončí úspěšně – nic se nemusí dělat.

  2. Akce skončí chybou – data se vrátí do předchozího stavu a zobrazí se chyba.

Používají se k tomu mutace, na které lze navěsit obslužnou logiku. Kód optimistických updatů už je trochu komplikovanější:

useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // Zrušíme probíhající refetch,
    // aby nepřepsal naši optimistickou změnu
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // Uložíme si předchozí hodnotu
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // Optimisticky nastavíme novou hodnotu
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // Vrátíme kontext s předchozími i novými daty
    return { previousTodo, newTodo }
  },
  // Pokud mutace selže, vrátíme se k předchozímu stavu
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo
    )
  },
  // Po úspěchu i neúspěchu (onSettled) provedeme refetch:
  onSettled: (newTodo) => {
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
  },
})

Je potřeba si rozmyslet, jestli to za to stojí.

  1. U závazných akcí to nemusí být úplně dobré řešení. Třeba u tlačítka pro dokončení objednávky.

  2. Pokud API odpovídá velmi rychle, třeba desítky milisekund, přínos nebude moc velký.

Kombinované query

Přes combine jde spojit data z více queries do jedné.

const ids = [1, 2, 3]
const combinedQueries = useQueries({
  queries: ids.map((id) => ({
    queryKey: ['post', id],
    queryFn: () => fetchPost(id),
  })),
  combine: (results) => {
    return {
      data: results.map((result) => result.data),
      pending: results.some((result) => result.isPending),
    }
  },
})

To se hodí hlavně v případě, že kdy je problematické přizpůsobit API požadavkům na frontendu.

Jde si tak celkem šikovně poskládat data z více API volání do jedné datové struktury.

Typický případ je napojování číselníků na ID. Jde si souběžně stáhnout všechna data, v combine je spojit dohromady a dál s nimi už pracovat jako s jedním celkem.

Integrace do JS frameworků

Kromě používání v čistém JS existují přímo napojení pro React, Solid, Vue, Svelte a Angular.

Není tedy problém TanStack Query zapojit do libovolného projektu. A není potřeba kvůli tomu přepisovat celou aplikaci. Jde s tím začít i postupně.

TanStack Query je poměrně rozšířený a existuje pro něj mnoho nástrojů. Dost zajímavé jsou různé generátory, které dokáží z API specifikace rovnou vytvořit hotové funkce pro volání API, které automaticky TanStack Query používají na pozadí.

To je všechno. Líbil se vám článek a chcete se dozvědět, až vyjde další?

Sledujte:

 

Připomínky mi pište do komentářů ↓

Framework Zepto.js

JavaScriptový framework Zepto.js

Odlehčená JS knihovna nabízející základní funkce jQuery ve zmenšené podobě.

Jak vytvořit WYSIWYG editor

Vlastní jednoduchý WYSIWYG editor

Chceme-li na webu zadávat text a běžná <textarea> už nestačí, řešením je napsat si vlastní WYSIWYG editor.

Detekce zapnutého JavaScriptu

Zapnutý a vypnutý JavaScript

Jak na webové stránce detekovat zapnuté nebo vypnuté skriptování.

CSS vyhledávání a filtrování

CSS vyhledávání a filtrování obsahu

Jak pomocí CSS se špetkou JS filtrovat obsah stránky nebo na ní vyhledávat.

Upozornění před opuštěním stránky

Upozornění před zavřením stránky

Javascriptová událost onbeforeunload umožňuje pozastavit uzavření/obnovení stránky.

Komentáře