Menu

Hook use w React: obietnice i kontekst podczas renderowania

use odczytuje wartość obietnicy albo kontekstu podczas renderowania. Z obietnicą zawiesza komponent, dopóki nie przyjdą dane, i pokazuje widok zastępczy najbliższego Suspense. W odróżnieniu od innych hooków może działać wewnątrz instrukcji if i pętli.

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

use to API z React 19, które odczytuje wartość z obietnicy albo z kontekstu podczas renderowania komponentu. use(promise) wstrzymuje komponent, dopóki obietnica się nie rozwiąże, a w tym czasie pokazuje widok zastępczy najbliższego <Suspense>; use(context) działa jak useContext, ale może stać wewnątrz if albo pętli.

Funkcja fetchUser poniżej zastępuje prawdziwe żądanie.

Pierwsze renderowanie przez sekundę pokazuje "Loading...", a potem profil. Kliknij User 2: widok zastępczy wraca, dopóki nowa obietnica oczekuje. Profile nie ma własnego stanu ładowania; jest napisany tak, jakby dane już były na miejscu.

Jak działa use(promise)

Gdy Profile wywołuje use(userPromise):

  • Jeśli obietnica się rozwiązała, use zwraca jej wartość, a renderowanie trwa dalej.
  • Jeśli wciąż oczekuje, React przestaje renderować Profile i pokazuje widok zastępczy najbliższego <Suspense> nad nim. Gdy obietnica się rozstrzygnie, React renderuje Profile ponownie i tym razem use zwraca wartość.
  • Jeśli została odrzucona, React pokazuje zamiast tego najbliższą granicę błędów.

React zapamiętuje wynik na samym obiekcie obietnicy. Dlatego przy następnym renderowaniu obietnica musi być tym samym obiektem.

Żeby zostawić stary profil na ekranie, gdy ładuje się następny, ustaw obietnicę wewnątrz przejścia. React wtedy czeka, zamiast znowu pokazywać widok zastępczy:

<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>User 2</button>

Gdzie tworzyć obietnicę

To jest część, na której ludzie się potykają. Napisanie wywołania fetch wewnątrz komponentu, który je odczytuje, wygląda naturalnie i nie działa:

// Do not do this
function Profile({ id }) {
    const user = use(fetchUser(id)); // a new promise on every render
    return <p>{user.name}</p>;
}

Każde renderowanie wywołuje fetchUser i dostaje nową, oczekującą obietnicę. Profile się zawiesza. Gdy ta obietnica się rozwiąże, React renderuje Profile ponownie, co tworzy kolejną oczekującą obietnicę, która znowu zawiesza komponent. Komponent nigdy nie pokazuje danych, a karta sieci zapełnia się żądaniami.

Twórz obietnicę tam, gdzie nie wykonuje się ponownie przy każdym renderowaniu komponentu, który ją czyta:

  • W rodzicu, trzymaną w stanie (jak w pierwszym przykładzie) albo utworzoną w handlerze zdarzenia.
  • Na poziomie modułu, gdy dane nie zależą od propsów: const configPromise = fetchConfig(); nad komponentem.
  • W cache z kluczem z argumentów, żeby to samo id zwracało tę samą obietnicę.
  • We frameworku: komponent serwerowy może przekazać obietnicę komponentowi klienckiemu jako prop, a loadery tras robią to samo. To przypadek, dla którego zaprojektowano use.

Oto wersja z cache, w której komponent może prosić o dane po id i nadal dostawać stabilną obietnicę:

Przejdź po trzech miastach, a potem wróć do takiego, które już otwierałeś. Konsola wypisuje jedno żądanie na miasto, a miasto, które już widziałeś, pojawia się bez widoku zastępczego, bo jego obietnica jest już rozwiązana. Usuń sprawdzenie if (!cache.has(city)), a komponent znowu będzie się zawieszał w nieskończoność.

Prawdziwy cache potrzebuje też sposobu na wygaszanie wpisów. Zajmują się tym biblioteki danych i frameworki, dlatego większość aplikacji dostaje obietnice właśnie od nich.

use(context), nawet wewnątrz if

use(SomeContext) zwraca tę samą wartość co useContext(SomeContext). Różnica dotyczy miejsca, w którym możesz go wywołać. Każdy inny hook musi wykonywać się na najwyższym poziomie, w tej samej kolejności przy każdym renderowaniu (zasady hooków). use może być wywołany po wczesnym return, wewnątrz warunku albo w pętli.

Kliknij przycisk, żeby przełączać się między dwiema gałęziami. Zamień use(UserContext) na useContext(UserContext), a kod złamie zasady hooków, bo hook wykonywałby się w niektórych renderowaniach, a w innych nie.

use nadal ma ograniczenia: wywołuj go tylko podczas renderowania komponentu albo wewnątrz własnego hooka. W handlerze zdarzenia albo efekcie korzystaj z wartości kontekstu, którą już dostałeś podczas renderowania.

Błędy: granice błędów, a nie try/catch

Gdy obietnica zostaje odrzucona, błąd trafia do najbliższej granicy błędów, tak samo jak błąd renderowania.

Otwórz Post 2, a granica pokaże komunikat o błędzie. Otwórz znowu Post 1, a zadziała: key={id} daje granicy świeży stan dla każdego posta, więc nie pokazuje dalej starego błędu.

Nie możesz złapać oczekującego ani odrzuconego use przez try/catch. Gdy obietnica oczekuje, use przerywa renderowanie, rzucając specjalną wartość, a catch wokół niego by ją połknął. Złapana wartość to błąd, którego komunikat zaczyna się od "Suspense Exception: This is not a real error!", a w trybie deweloperskim React wypisuje też "use was called from inside a try/catch block". Żeby zamiast ekranu błędu pokazać wartość zapasową, obsłuż odrzucenie na obietnicy, zanim przekażesz ją w dół:

const postPromise = fetchPost(id).catch(() => 'This post could not be loaded.');

use w porównaniu z pobieraniem w useEffect

Klasyczny wzorzec po stronie klienta pobiera dane w efekcie i trzyma trzy kawałki stanu:

function Profile({ id }) {
    const [user, setUser] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        let ignore = false;
        fetchUser(id).then(
            (u) => !ignore && setUser(u),
            (e) => !ignore && setError(e),
        );
        return () => {
            ignore = true;
        };
    }, [id]);

    if (error) return <p>Error</p>;
    if (!user) return <p>Loading...</p>;
    return <p>{user.name}</p>;
}
useEffectuse
Kiedy zaczyna się żądaniepo tym, jak pierwsze renderowanie jest na ekraniegdy obietnica zostaje utworzona, czasem zanim komponent się wyrenderuje
Stan ładowaniatwoje własne if (!user)najbliższe <Suspense>
Błędytwój własny stan errornajbliższa granica błędów
Wyścigi (race conditions)sam się przed nimi chronisz (ignore)komponent zawsze czyta obietnicę, którą dostał
Kto tworzy obietnicękomponentrodzic, cache albo framework

Wersja z efektem jest samowystarczalna i dlatego wciąż jest najczęstszym podejściem w aplikacjach bez frameworka; strona o pobieraniu danych omawia ją w całości. use błyszczy, gdy coś nad komponentem wcześnie rozpoczyna żądanie i przekazuje obietnicę w dół.

Najczęściej zadawane pytania

Co robi hook use w React?

use(resource) zwraca wartość obietnicy albo kontekstu. Z obietnicą komponent zawiesza się, dopóki się nie rozwiąże, a najbliższe <Suspense> pokazuje swój widok zastępczy; jeśli zostanie odrzucona, zamiast tego pokazuje się najbliższa granica błędów.

Czy use można wywołać warunkowo?

Tak. use to jedyny hook, który możesz wywołać w instrukcjach if, pętlach i po wczesnym return. Nadal trzeba go wywoływać z komponentu albo hooka, nigdy z handlera zdarzenia ani efektu.

Dlaczego use(fetch()) w moim komponencie zapętla się w nieskończoność?

Każde renderowanie tworzy nową obietnicę. Komponent się na niej zawiesza, React renderuje ponownie, gdy się rozwiąże, a to renderowanie tworzy kolejną oczekującą obietnicę. Twórz obietnicę poza renderowaniem: w stanie rodzica, na poziomie modułu, w cache albo w loaderze frameworka.

Czy mogę owinąć use w try/catch?

Nie. Gdy obietnica oczekuje, use przerywa renderowanie, rzucając wyjątek, a jego złapanie to psuje. Odrzuconą obietnicę obsłuż granicą błędów albo dołącz do obietnicy .catch(), żeby rozwiązała się do wartości zapasowej.

Czy zastąpić pobieranie danych w useEffect przez use?

use potrzebuje obietnicy, która jest tworzona raz i używana ponownie, a to dają frameworki i biblioteki danych. W zwykłej aplikacji klienckiej pobieranie w efekcie (albo biblioteką taką jak TanStack Query) nadal jest powszechne i w porządku.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ