Hook use w React 19 – koniec z loading/error state boilerplate 🎉

Jeśli zdarzyło Wam się pisać w React komponent, który pobiera dane z API, to pewnie dobrze znacie ten schemat. Trzy stany, useEffect, obsługa błędów, obsługa ładowania… i to wszystko zanim jeszcze dojdziemy do właściwej logiki komponentu.

Przykład:

function UserListClassic({ url }) {
  const [data, setData]       = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError]     = useState(null)

  useEffect(() => {
    fetch(url)
      .then(res => {
        if (!res.ok) {
          throw new Error(`HTTP ${res.status}`)
        }
        return res.json()
      })
      .then(json => {
        setData(json)
        setLoading(false)
      })
      .catch(err => {
        setError(err.message)
        setLoading(false)
      })
  }, [url])

  if (loading) return <Spinner />
  if (error)   return <ErrorMsg error={error} />
  return <UserList data={data} />
}

Około 27 linii, z czego większość to zarządzanie stanem – a nie faktyczna praca komponentu. Nie jest to nic złego, ale React 19 przynosi nam sposób na pozbycie się tego całego boilerplate’u.

Czym jest use?

use to nowe API wprowadzone w React 19, które pozwala odczytać wartość z Promise lub z kontekstu bezpośrednio w ciele komponentu.

const value = use(resource)

W odróżnieniu od hooków, use można wywoływać warunkowo – w pętlach, w blokach if, w różnych miejscach komponentu. To wyjątek od reguły hooków i jedna z rzeczy, które wyróżniają use spośród pozostałych API Reacta.

Kiedy przekazujemy mu Promise, React “zawiesza” renderowanie komponentu do czasu, aż dane będą gotowe. Jeśli fetch zwróci błąd, React sam go przechwytuje i przekazuje do najbliższego Error Boundary. Zamiast samodzielnie obsługiwać oba przypadki w komponencie, delegujemy to do Reacta.

Ważne: use nie zastępuje useEffect w ogólności. Rozwiązuje konkretny scenariusz – “komponent czeka na Promise”. Jeśli potrzebujesz efektów ubocznych, subskrypcji, timerów czy czegokolwiek innego niezwiązanego z oczekiwaniem na dane – useEffect nadal jest właściwym narzędziem.

Jak to wygląda w praktyce?

Zobaczcie jak wygląda ten sam komponent przepisany z użyciem use:

function UserListModern({ promise }) {
  const data = use(promise)
  return <UserList data={data} />
}

Trzy linie!

Komponent zajmuje się tylko tym, co ma wyświetlić – nie interesuje go ani ładowanie, ani błędy. Te dwa przypadki obsługuje jego kontener:

<ErrorBoundary>             {/* ← obsługuje błąd */}
  <Suspense fallback={...}> {/* ← obsługuje ładowanie */}
    <UserListModern promise={promise} />
  </Suspense>
</ErrorBoundary>

<Suspense> wyświetli fallback dopóki Promise się nie rozwiąże, a <ErrorBoundary> przechwytuje błędy z odrzuconego Promise.

Demo z projektu

Stworzyłam mały projekt na Github, który pokazuje oba podejścia obok siebie.

Po lewej klasyczny useState + useEffect, po prawej use z Suspense. Oba pobierają te same dane z tego samego URL, a różnica jest wyłącznie w tym, kto zarządza stanami.

Żeby to zobaczyć, wystarczy zajrzeć do dwóch komponentów: WithUseState.tsx i WithUse.tsx

Kilka ważnych szczegółów

Jak wygląda minimalny ErrorBoundary?

ErrorBoundary to nadal class component – React nie udostępnia jeszcze wbudowanego komponentu tego typu, więc trzeba go napisać samodzielnie (albo użyć paczki react-error-boundary):

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null }

  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? <p>Coś poszło nie tak.</p>
    }
    return this.props.children
  }
}

Jeśli nie chcecie pisać tego ręcznie, polecam gotową paczkę react-error-boundary, która daje też wygodny hook useErrorBoundary.

Promise musi być stabilny

Promise trzeba tworzyć poza komponentem (albo memoizować), ponieważ jeśli będziemy go tworzyć bezpośrednio w render, React za każdym renderowaniem dostanie nowy Promise i będzie wpadać w nieskończoną pętlę. W demo powyżej Promise jest tworzony w komponencie-rodzicu i przekazywany jako props – to dobre podejście.

Jeśli jednak musisz tworzyć Promise wewnątrz komponentu, użyj useMemo.

function ParentComponent({ url }) {
  const promise = useMemo(() => fetch(url).then(res => res.json()), [url])

  return (
    <ErrorBoundary fallback={<ErrorMsg />}>
      <Suspense fallback={<Spinner />}>
        <UserListModern promise={promise} />
      </Suspense>
    </ErrorBoundary>
  )
}

use działa też z kontekstem

use można stosować zamiast useContext – z jedną istotną różnicą: w odróżnieniu od useContext, wywołanie use(MyContext) może pojawić się warunkowo:

// klasycznie – tylko na poziomie komponentu, nigdy warunkowo
const theme = useContext(ThemeContext)

// z use – można warunkowo
function MyComponent({ applyTheme }) {
  if (applyTheme) {
    const theme = use(ThemeContext)
    return <div style={{ color: theme.primary }}>...</div>
  }
  return <div>...</div>
}

Czy warto przejść na use?

To zależy od projektu. Jeśli używacie React 19 i macie już <Suspense> w swojej architekturze (albo planujecie ją wprowadzić), use znacznie upraszcza komponenty i sprawia, że każdy z nich odpowiada za jedną rzecz – wyświetlanie danych, a nie zarządzanie ich stanem pobierania.

Jeśli macie React ≤ 18 albo potrzebujecie bardziej szczegółowej kontroli nad stanami ładowania w samym komponencie – klasyczne podejście nadal działa i nie ma nic złego w jego używaniu:)

Pełna dokumentacja jest dostępna na https://react.dev/reference/react/use.

You might also like