Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Cachowanie danych i Stale-While-Revalidate

Wyobraź sobie, że Twoj statek kosmiczny wielokrotnie pyta Centrum Dowodzenia o te same dane o planetach. Za kazdym razem czekasz na odpowiedz, nawet jesli dane sie nie zmienily. To marnotrawstwo! Rozwiazaniem jest cachowanie -- przechowywanie danych lokalnie i odswiezanie ich w tle.

Dlaczego cachowanie jest wazne?

Bez cache:

  • Kazda nawigacja = nowe zapytanie = spinner
  • Uzytkownik widzi "Ladowanie..." nawet dla danych, które juz widzial
  • Zbedne obciazenie serwera i sieci

Z cache:

  • Dane wyswietlane natychmiast z pamieci
  • Odswiezanie w tle (bez spinnera)
  • Lepsze doswiadczenie uzytkownika

Wzorzec Stale-While-Revalidate (SWR)

SWR to strategia cachowania uzywana przez przegladarki i biblioteki. Nazwa mowi wszystko:

  1. Stale -- natychmiast pokaz dane z cache (nawet jesli moga byc nieaktualne)
  2. While-Revalidate -- jednoczesnie pobierz swieze dane z serwera w tle
  3. Po otrzymaniu swiezych danych -- zaktualizuj widok
1// Wizualizacja SWR:
2// Krok 1: Uzytkownik wchodzi na strone
3//   -> Cache istnieje? TAK -> Pokaz dane z cache (natychmiast!)
4//   -> Jednoczesnie: wysylaj zapytanie do serwera
5
6// Krok 2: Serwer odpowiada
7//   -> Dane sie zmienily? TAK -> Zaktualizuj widok i cache
8//   -> Dane takie same? -> Nic nie rob
9
10// Krok 3: Uzytkownik wraca na strone
11//   -> Powtorz od Kroku 1 (natychmiastowe wyswietlenie!)

Prosty cache z Map

Najprostszy cache mozna zbudowac z obiektu

Map
:

1// Globalny cache - zyje poza komponentami
2const cache = new Map();
3
4function useFetchWithCache(url, ttl = 60000) {
5  const [data, setData] = useState(() => {
6    const cached = cache.get(url);
7    if (cached && Date.now() - cached.timestamp < ttl) {
8      return cached.data; // Natychmiastowe dane z cache!
9    }
10    return null;
11  });
12  const [loading, setLoading] = useState(!cache.has(url));
13  const [error, setError] = useState(null);
14
15  useEffect(() => {
16    const controller = new AbortController();
17
18    async function fetchData() {
19      try {
20        // Jesli mamy cache - nie pokazuj spinnera
21        if (!cache.has(url)) setLoading(true);
22        setError(null);
23
24        const response = await fetch(url, {
25          signal: controller.signal,
26        });
27        if (!response.ok) {
28          throw new Error(\`HTTP \${response.status}\`);
29        }
30        const result = await response.json();
31
32        // Zapisz w cache z timestampem
33        cache.set(url, {
34          data: result,
35          timestamp: Date.now(),
36        });
37        setData(result);
38      } catch (err) {
39        if (err.name !== 'AbortError') {
40          setError(err.message);
41        }
42      } finally {
43        setLoading(false);
44      }
45    }
46
47    fetchData();
48    return () => controller.abort();
49  }, [url, ttl]);
50
51  return { data, loading, error };
52}

TTL (Time To Live) - czas zycia cache

TTL okresla, jak dlugo dane w cache sa uznawane za aktualne:

1// TTL = 60000ms (1 minuta)
2// Dane starsze niz 1 minuta zostana pobrane ponownie
3
4// Krotki TTL (5-30s) - czesto zmieniajace sie dane
5// np. ceny kryptowalut, status misji
6const liveData = useFetchWithCache('/api/status', 5000);
7
8// Sredni TTL (1-5 min) - umiarkowanie zmienne dane
9// np. lista planet, katalog statkow
10const catalog = useFetchWithCache('/api/planets', 60000);
11
12// Dlugi TTL (10-60 min) - rzadko zmieniajace sie dane
13// np. konfiguracja, statyczne listy
14const config = useFetchWithCache('/api/config', 600000);

Invalidacja cache - wymuszenie odswiezenia

Czasem musisz wymusic pobranie swiezych danych (np. po dodaniu nowego elementu):

1// Prosta invalidacja - usun z cache
2function invalidateCache(url) {
3  cache.delete(url);
4}
5
6// Po dodaniu nowej misji - invaliduj liste misji
7async function createMission(data) {
8  await fetch('/api/missions', {
9    method: 'POST',
10    headers: { 'Content-Type': 'application/json' },
11    body: JSON.stringify(data),
12  });
13
14  // Wymus odswiezenie listy misji
15  invalidateCache('/api/missions');
16}

Optimistic Updates - aktualizacja przed odpowiedzia

Zamiast czekac na serwer, mozemy zaktualizowac UI natychmiast:

1function MissionList() {
2  const [missions, setMissions] = useState([]);
3
4  const deleteMission = async (id) => {
5    // 1. OPTYMISTYCZNIE usun z UI (natychmiast!)
6    const previousMissions = [...missions];
7    setMissions(prev => prev.filter(m => m.id !== id));
8
9    try {
10      // 2. Wysylaj zapytanie DELETE do serwera
11      await fetch(\`/api/missions/\${id}\`, {
12        method: 'DELETE',
13      });
14      // Sukces - UI juz zaktualizowany!
15    } catch (error) {
16      // 3. BLAD - przywroc poprzedni stan (rollback)
17      setMissions(previousMissions);
18      alert('Nie udalo sie usunac misji');
19    }
20  };
21
22  return (
23    <ul>
24      {missions.map(m => (
25        <li key={m.id}>
26          {m.name}
27          <button onClick={() => deleteMission(m.id)}>
28            Usun
29          </button>
30        </li>
31      ))}
32    </ul>
33  );
34}

TanStack Query -- profesjonalne cachowanie

W duzych projektach zamiast pisac wlasny cache, uzywamy biblioteki TanStack Query (dawniej React Query). To jak zaawansowany komputer pokladowy, ktory automatycznie zarzadza cala komunikacja:

1import {
2  useQuery,
3  useMutation,
4  useQueryClient,
5  QueryClient,
6  QueryClientProvider,
7} from '@tanstack/react-query';
8
9const queryClient = new QueryClient();
10
11function App() {
12  return (
13    <QueryClientProvider client={queryClient}>
14      <PlanetList />
15    </QueryClientProvider>
16  );
17}
18
19function PlanetList() {
20  // useQuery - automatyczny cache, SWR, retry, refetch
21  const { data, isLoading, error } = useQuery({
22    queryKey: ['planets'],
23    queryFn: () =>
24      fetch('https://swapi.dev/api/planets/')
25        .then(res => res.json()),
26    staleTime: 60000, // Dane swieze przez 1 minute
27    gcTime: 300000, // Usun z cache po 5 minutach
28  });
29
30  if (isLoading) return <p>Skanowanie galaktyki...</p>;
31  if (error) return <p>Blad: {error.message}</p>;
32
33  return (
34    <ul>
35      {data.results.map(planet => (
36        <li key={planet.name}>{planet.name}</li>
37      ))}
38    </ul>
39  );
40}

TanStack Query - kluczowe koncepcje

1// 1. queryKey - unikalny identyfikator danych w cache
2//    Zmiana klucza = nowe zapytanie
3const planets = useQuery({
4  queryKey: ['planets', page],
5  queryFn: () => fetchPlanets(page),
6});
7
8// 2. staleTime - jak dlugo dane sa swieze
9//    staleTime: 0 - zawsze odswiezaj (domyslnie)
10//    staleTime: 60000 - swieze przez 1 minute
11//    staleTime: Infinity - nigdy nie odswiezaj
12
13// 3. gcTime (garbage collection) - kiedy usunac z cache
14//    gcTime: 300000 - usun po 5 min nieaktywnosci
15
16// 4. Automatyczny refetch:
17//    - Gdy okno wraca do fokusu
18//    - Gdy siec wraca po rozlaczeniu
19//    - W regularnych odstepach (refetchInterval)
20
21// 5. useMutation - operacje zapisu z invalidacja
22const mutation = useMutation({
23  mutationFn: (newMission) =>
24    fetch('/api/missions', {
25      method: 'POST',
26      body: JSON.stringify(newMission),
27    }),
28  onSuccess: () => {
29    // Po dodaniu misji - invaliduj cache listy misji
30    queryClient.invalidateQueries({
31      queryKey: ['missions']
32    });
33  },
34});

Cache to supermoc kazdego kosmicznego nawigatora -- dane sa dostepne natychmiast, serwer nie jest przeciazony, a uzytkownik nigdy nie czeka na dane, ktore juz widzial!

Ir a CodeWorlds