Menu

Pobieranie danych w React z useEffect: ładowanie i błędy

Pobieraj dane w React, uruchamiając żądanie w useEffect, trzymając ładowanie, błąd i dane w stanie i ignorując odpowiedzi, które przychodzą po zmianie danych wejściowych. Omawiamy async/await w efektach, AbortController, wyścigi (race conditions) i to, kiedy zamiast tego użyć biblioteki.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Żeby pobrać dane w React, uruchom żądanie wewnątrz useEffect, zapisz odpowiedź w stanie i renderuj komunikat ładowania, dopóki nie przyjdzie. Wartości, od których zależy żądanie, na przykład id, umieść w tablicy zależności, żeby efekt pobierał dane ponownie, gdy się zmienią.

Przykłady na tej stronie nie mają dostępu do sieci, więc fetchUser to udawane API: obietnica, która rozwiązuje się po opóźnieniu, tak jak robi to fetch. Zmień 800 na 3000, żeby tekst ładowania został dłużej.

Z prawdziwym API ciało efektu wygląda tak:

useEffect(() => {
    fetch('https://api.example.com/users/1')
        .then((res) => res.json())
        .then((data) => setUser(data));
}, []);

Ładowanie, błąd i dane

Żądanie ma trzy wyniki, które powinien pokazać ekran: wciąż się ładuje, nie powiodło się albo się zakończyło. Trzymaj każdy w stanie i resetuj je, gdy efekt rozpoczyna nowe żądanie.

Kliknij User 3: udawane API odrzuca obietnicę, komunikat o błędzie zastępuje imię, a konsola pokazuje błąd. Blok finally czyści loading na obu ścieżkach, więc nieudane żądanie nigdy nie zostawia włączonego spinnera.

Gdy fetch rozmawia z prawdziwym serwerem, odpowiedź 404 albo 500 nie odrzuca obietnicy. Sprawdź res.ok i sam rzuć błąd:

const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

async/await wewnątrz efektu

Funkcja, którą przekazujesz do useEffect, nie może być async. React oczekuje, że zwróci nic albo funkcję czyszczącą, a funkcja async zawsze zwraca obietnicę.

// Wrong: the effect returns a promise
useEffect(async () => {
    const data = await fetchUser(id);
    setUser(data);
}, [id]);

// Right: define an async function inside and call it
useEffect(() => {
    async function load() {
        const data = await fetchUser(id);
        setUser(data);
    }
    load();
}, [id]);

Wyścigi (race conditions)

Gdy id szybko się zmienia, naraz trwają dwa żądania. Nic nie gwarantuje, że odpowiedzą po kolei. Jeśli starsze jest wolniejsze, przychodzi ostatnie i nadpisuje dane dla id, które użytkownik wybrał jako ostatnie.

Rozwiązaniem jest flaga ignore. Każde uruchomienie efektu ma własną flagę, a jego funkcja czyszcząca ustawia ją na true. Ponieważ React uruchamia funkcję czyszczącą przed następnym uruchomieniem, odpowiedź z nieaktualnego uruchomienia widzi ignore === true i zostaje odrzucona. Ten blok renderuje ten sam profil dwa razy, bez flagi i z flagą:

Kliknij przycisk i poczekaj. Szybka odpowiedź dla użytkownika 2 przychodzi pierwsza, więc oba profile ją wypisują i zostają przy Grace. Około półtorej sekundy po kliknięciu przychodzi wolna odpowiedź dla użytkownika 1: profil bez flagi przełącza się na Adę, choć wybrany jest użytkownik 2, a ten z flagą wypisuje dropped stale Ada i zostaje przy Grace.

Ten sam błąd pojawia się w polach wyszukiwania, kartach i każdej liście filtrowanej przez serwer. Przykład z ładowaniem powyżej pomija flagę, żeby był krótki, ale każde pobieranie w efekcie powinno ją mieć albo korzystać z anulowania opisanego niżej.

Anulowanie przez AbortController

Flaga ignore odrzuca nieaktualną odpowiedź, ale żądanie i tak wykonuje się do końca. Z prawdziwym fetch możesz je anulować. Utwórz AbortController w efekcie, przekaż jego signal do fetch i wywołaj abort() w funkcji czyszczącej:

useEffect(() => {
    const controller = new AbortController();

    async function load() {
        try {
            const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            setUser(await res.json());
        } catch (err) {
            if (err.name === 'AbortError') return; // cancelled on purpose
            setError(err);
        }
    }

    load();
    return () => controller.abort();
}, [id]);

Anulowany fetch odrzuca obietnicę z AbortError, więc blok catch go pomija, zamiast pokazywać jako błąd. Anulowanie obejmuje też odmontowanie: gdy komponent zostaje usunięty, jego funkcja czyszcząca anuluje żądanie.

Przeniesienie do własnego hooka

Ładowanie, błąd i ochrona przed wyścigiem są takie same w każdym komponencie, który pobiera dane. Umieść je we własnym hooku, a każdy komponent poprosi o dane w jednej linii.

fetchPosts jest zdefiniowane poza komponentem, więc jego tożsamość nigdy się nie zmienia i można je bezpiecznie umieścić w tablicy zależności. Zdefiniowane wewnątrz App byłoby nową funkcją przy każdym renderowaniu, efekt uruchamiałby się po każdym renderowaniu, a ponieważ efekt ustawia stan, nigdy nie przestałby pobierać danych.

Zmień 500 na 2000 i kliknij przycisk dwa razy z rzędu: konsola pokazuje dwa żądania, a pojawiają się tylko posty dla tematu, na którym skończyłeś.

Pobieranie w handlerze zdarzenia

Efekt służy do danych, których komponent potrzebuje, bo jest na ekranie: strona profilu ładuje swój profil. Gdy żądanie wynika z tego, że użytkownik coś zrobił, na przykład nacisnął Szukaj albo Zapisz, wykonaj je w handlerze zdarzenia. Dokładnie wiesz, co je wywołało, i nic nie uruchamia się ponownie, gdy zmienia się niezwiązany stan.

async function handleSubmit(e) {
    e.preventDefault();
    setStatus('saving');
    const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
    setStatus(res.ok ? 'saved' : 'error');
}

Częste błędy

Brakujące zależności. Efekt, który czyta id, ale ma [], w nieskończoność pobiera pierwszego użytkownika. Wymień każdą wartość, której używa żądanie.

Pobieranie w ciele komponentu. fetch poza efektem wykonuje się przy każdym renderowaniu, a jeśli ustawia stan, uruchamia pętlę.

Liczenie na to, że fetch odrzuci obietnicę przy błędach. Odrzuca ją tylko wtedy, gdy zawiedzie sieć. Sprawdzaj res.ok.

Zapomnienie o resecie ładowania. Gdy id się zmienia, ustaw loading z powrotem na true, bo inaczej stare dane zostaną na ekranie bez żadnego sygnału, że nowe są w drodze.

Kiedy użyć biblioteki albo frameworka

Pobieranie w efekcie wystarcza przy kilku żądaniach. Nie ma cache: otwórz ten sam profil dwa razy, a pobierze go dwa razy. Nie współdzieli danych między komponentami, nie ponawia po błędzie i nie pobiera ponownie, gdy karta znów zyska fokus. Biblioteki robią to za ciebie:

import { useQuery } from '@tanstack/react-query';

function Profile({ id }) {
    const { data, error, isPending } = useQuery({
        queryKey: ['user', id],
        queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
    });

    if (isPending) return <p>Loading...</p>;
    if (error) return <p>{error.message}</p>;
    return <p>{data.name}</p>;
}

TanStack Query i SWR to popularne wybory do danych po stronie klienta. Frameworki idą dalej i ładują dane na serwerze, zanim strona dotrze do przeglądarki: Next.js z komponentami serwerowymi, React Router z loaderami. To eliminuje mignięcie ładowania i łańcuch żądań, który powstaje, gdy dane rodzica muszą przyjść, zanim dziecko w ogóle zacznie pobierać.

React 19: use() z Suspense

React 19 dodaje use, które odczytuje obietnicę podczas renderowania. Komponent zawiesza się, dopóki obietnica się nie rozwiąże, a najbliższa granica <Suspense> pokazuje w tym czasie zastępczy widok (fallback), więc sam komponent nie ma stanu ładowania:

import { use, Suspense } from 'react';

function Profile({ userPromise }) {
    const user = use(userPromise);
    return <p>{user.name}</p>;
}

<Suspense fallback={<p>Loading...</p>}>
    <Profile userPromise={userPromise} />
</Suspense>

Obietnica musi zostać utworzona poza komponentem (przez framework, cache albo rodzica), a nie w renderowaniu, które ją odczytuje, bo inaczej każde renderowanie rozpocznie nowe żądanie. Strona o hooku use omawia to na działających przykładach.

Najczęściej zadawane pytania

Jak pobrać dane, gdy ładuje się komponent Reacta?

Uruchom żądanie w useEffect, z wartościami, od których zależy, w tablicy zależności ([], jeśli nie ma żadnych), i zapisz wynik w stanie przez useState. Renderuj komunikat ładowania, dopóki dane nie przyjdą.

Dlaczego callback useEffect nie może być async?

Funkcja async zawsze zwraca obietnicę, a React oczekuje, że efekt zwróci albo nic, albo funkcję czyszczącą. Napisz funkcję async wewnątrz efektu i od razu ją wywołaj.

Co to jest race condition przy pobieraniu danych w React?

Gdy dane wejściowe szybko się zmieniają, w toku są dwa żądania, a starsze może odpowiedzieć ostatnie i nadpisać nowsze dane. Ustaw flagę ignore w funkcji czyszczącej efektu i pomiń setState, gdy jest ustawiona, albo anuluj żądanie przez AbortController.

Czy do pobierania danych używać useEffect, czy biblioteki?

useEffect działa w małych aplikacjach i warto go rozumieć. Do cache'owania, deduplikacji, ponawiania i ponownego pobierania biblioteka taka jak TanStack Query albo SWR, albo ładowanie danych wbudowane we framework taki jak Next.js, oszczędza ci pisania tej logiki samemu.

Jak pokazać spinner, gdy dane się ładują?

Trzymaj w stanie wartość logiczną loading (albo tekst status), ustaw ją przed rozpoczęciem żądania i wyczyść po jego zakończeniu, zarówno przy sukcesie, jak i przy błędzie. Renderuj spinner, dopóki jest prawdziwa.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ