TanStack Query (CZ článek)

Back to blog

Praktický úvod do využití TanStack Query v Reactu s TypeScriptem

5 min read
Table of Contents

Tanstack Query je moderní state manager pro TS / JS aplikace, se skvělými funkcemi na vylepšení DX ale i zkvalitnění UX skrz spolehlivější načítání dat.

Tady odkazy na dokumentaci, na přehled funkcí, na oficiální studijní kurz (placený), a na kurz na internetu zdarma.

Vlastními slovy se popisuje:

Powerful asynchronous state management for TS/JS, React, Solid, Vue, Svelte and Angular

TanStack Query (někteří si možná pamatují pod názvem React Query) ti usnadní práci načítání dat z API, správu loading a error stavů, cachování nebo refetching. Už žádné ruční useEffect, loading stavy, ani složité cachování.

Co Tanstack Query umí:

  • načítáni dat (useQuery)
  • automaticky spravuje loading/error stavy
  • ukládá a sdílí data v cache
  • refetchuje podle potřeby - po znovunačtení okna, změně focusu apod.
  • mutace (useMutation)

a navíc v jednom balíčku poskytuje také:

  • Tanstack Query Devtools – realtime náhled na cache a dotazy
  • SSR & React Native podpora

Ukázka nejjednoduššího použití Tanstack Query

Podívejme se na oficiální ukázku

import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const { isPending, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/TanStack/query').then((res) =>
        res.json(),
      ),
  })

  if (isPending) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>✨ {data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  )
}
  • nainstaluješ a zabalíš svoji aplikaci do QueryProvider a hotovo, je to funkční
  • v komponentě využiješ useQuery, které odevzdáš funkci která fetchuje tvoje data, o vše ostatní se stará React Query, a také klíč díky kterému bude hlídat aktuální data
  • tento hook ti poskytne vše co v komponentě potřebuješ: loading, isPending nebo error( a hromadu dalších pomocníků, viz dokumentaci)
  • jednoduše vyrenderuješ na základě stavu query různé UI
  • když data šťastně dorazí, tak renderuješ data

Podívejme se ale i na podrobnější příklad jak v praxi využít tento super nástroj.

Ukázka z praxe

Prakticky je velmi příjemné si držet všechny fetch funkce na jednom místě jako api.ts nebo query.ts, následně doporučované a vynikající DX je využívat custom hooks, které složíme pomocí Query funkcí. Odstraníme tím hromadu boilerplate a komponenty jsou pak mnohem čistší a mnoho z nich se stane takřka dumb komponentami.

// queries.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

export type Restaurant = {
  id: string;
  name: string;
  address: string;
};

// Fetcher: načte seznam restaurací
// klidně využívej fetchovací knihovnu jako `axios` nebo `ky`
const fetchRestaurants = async (): Promise<Restaurant[]> => {
  const res = await fetch('/api/restaurants');
  if (!res.ok) throw new Error('Chyba při načítání restaurací');
  return res.json();
};

// Fetcher: vytvoří novou restauraci
const postRestaurant = async (data: Omit<Restaurant, 'id'>): Promise<Restaurant> => {
  const res = await fetch('/api/restaurants', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error('Chyba při přidávání restaurace');
  return res.json();
};

// Custom hook pro načítání seznamu
// vrátí objekt s daty (nebo null) a různými pomocníky typu boolean nebo Error, které automaticky sleduje, trackuje a ty na základě nich můžeš jen renderovat co potřebuješ
// např. data, isError, error, isPending, loading, status, isFetched, isStale, refetch
export const useRestaurants = () => {
  return useQuery<Restaurant[]>({
    queryKey: ['restaurants'], // VELICE důležité!!
    queryFn: fetchRestaurants,
    staleTime: 2 * 1000  // čas (v ms) který se data budou vracet z cache, místo nového volání API
  });
}

// Custom hook pro mutaci (vytvoření nové restaurace)
export const useAddRestaurant = () => {
  const queryClient = useQueryClient();

   // vrátí metody na správu akce POST (nebo jakékoliv jiné) mutation dle poskytnuté logiky
  return useMutation({
    mutationFn: postRestaurant,
    onSuccess: () => {
      // Po úspěšném přidání invaliduj seznam, aby se znovu načetl
      // TADY využiješ ten klíč, který si poskytoval původní useQuery funkci, a díky nemu jsou query provázané
      // doporučuji držet všechny `queryKeys` na jednom místě jako read-only objekt nebo enum
      queryClient.invalidateQueries({ queryKey: ['restaurants'] });
      // když je hotovo, seznam se refetchuje z API (díky invalidaci) a RestaurantList se rerenderuje
    },
  });
};
// RestaurantList.tsx
import { useRestaurants } from './queries';

export const RestaurantList = () => {
  const { data, isLoading, error } = useRestaurants();

  if (isLoading) return <p>Načítání restaurací...</p>;
  if (error) return <p>Chyba: {(error as Error).message}</p>;

  return (
    <ul>
      {data?.map((r) => (
        <li key={r.id}>
          <strong>{r.name}</strong> – {r.address}
        </li>
      ))}
    </ul>
  );
};
// AddRestaurantForm.tsx
import { useState } from 'react';
import { useAddRestaurant } from './queries';

export const AddRestaurantForm = () => {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  const mutation = useAddRestaurant(); // tady metody na správu `action`, ale i pomocníci s info o stavu query

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!name || !address) return;

    mutation.mutate({ name, address });
    setName('');
    setAddress('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text"
        placeholder="Název restaurace"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input type="text"
        placeholder="Adresa"
        value={address}
        onChange={(e) => setAddress(e.target.value)}
      />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Přidávám...' : 'Přidat'}
      </button>
      {mutation.error && (
        <p style={{ color: 'red' }}>
          {(mutation.error as Error).message}
        </p>
      )}
    </form>
  );
};
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RestaurantList } from './RestaurantList';
import { AddRestaurantForm } from './AddRestaurantForm';

const queryClient = new QueryClient();

export const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <h1>Restaurace</h1>
      <AddRestaurantForm />
      <RestaurantList />
    </QueryClientProvider>
  );
};

Další vlastnosti

Díky Tanstack Query můžeme relativně jednoduše, ale s výborným DX, zpracovávat i Infinite Scrolling nebo Optimistic updates, pro víc info koukni na useInfiniteQuery nebo na termín Optimistic updates v dokumentaci. A ještě stále je možné říct, že Tanstack Query toho dokáže mnohem mnohem víc.