Back

React 19 use() Hook Completo: A Mudança de Paradigma no Data Fetching

O React 19 trouxe várias funcionalidades novas, mas uma se destaca por como muda fundamentalmente a forma que escrevemos código React: o hook use().

Se você trabalha com React há algum tempo, provavelmente já escreveu esse padrão centenas de vezes:

function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function fetchUser() { try { setLoading(true); const response = await fetch(`/api/users/${userId}`); const data = await response.json(); if (!cancelled) { setUser(data); } } catch (err) { if (!cancelled) { setError(err); } } finally { if (!cancelled) { setLoading(false); } } } fetchUser(); return () => { cancelled = true; }; }, [userId]); if (loading) return <Spinner />; if (error) return <Error message={error.message} />; return <div>{user.name}</div>; }

São 40 linhas de código pra algo conceitualmente simples: "buscar um usuário e mostrar o nome dele."

Com o hook use() do React 19, aqui tá o equivalente:

function UserProfile({ userId }) { const user = use(fetchUser(userId)); return <div>{user.name}</div>; }

3 linhas. Mesma funcionalidade. Sem gerenciamento de estado de loading. Sem boilerplate de tratamento de erro. Sem bugs de race condition.

Isso não é só açúcar sintático—representa uma mudança fundamental em como o React lida com operações assíncronas. Vamo explorar como funciona, quando usar, e os gotchas que você precisa conhecer.

O que é o Hook use()?

O hook use() é a forma do React 19 de ler valores de recursos como Promises ou Contexts. Diferente de outros hooks, o use() tem poderes especiais:

  1. Pode ser chamado condicionalmente (dentro de if statements, loops, etc.)
  2. Se integra com Suspense pra estados de loading
  3. Se integra com Error Boundaries pra tratamento de erros
  4. Funciona com Promises pra "desembrulhar" valores async

Aqui tá a assinatura básica:

import { use } from 'react'; // Com Promises const value = use(promise); // Com Context const theme = use(ThemeContext);

Como o use() Funciona Por Baixo dos Panos

Pra entender o use(), você precisa entender como o mecanismo Suspense do React funciona.

Quando você chama use(promise):

  1. Se a promise tá pending: O React "suspende" o componente. Ele joga um objeto especial que o Suspense captura, disparando o UI de fallback.

  2. Se a promise tá resolvida: O React retorna o valor resolvido imediatamente.

  3. Se a promise foi rejeitada: O React joga o erro, que o Error Boundary captura.

Aqui tá um modelo mental simplificado:

function use(promise) { if (promise.status === 'pending') { throw promise; // Suspense captura isso } if (promise.status === 'rejected') { throw promise.reason; // Error Boundary captura isso } return promise.value; // Retorna valor resolvido }

O insight chave: use() não gerencia estado—ele lê de um recurso e diz pro React o que fazer com o resultado.

O Padrão Crítico: Cachear Promises

Aqui é onde muitos devs se confundem. Isso NÃO VAI FUNCIONAR:

// ❌ ERRADO: Cria nova promise em cada render function UserProfile({ userId }) { const user = use(fetch(`/api/users/${userId}`).then(r => r.json())); return <div>{user.name}</div>; }

Por quê? Porque toda vez que o componente renderiza, você cria uma nova Promise. O React vê uma nova Promise, suspende, o fallback aparece, a Promise resolve, o React re-renderiza, cria uma nova Promise... loop infinito.

Promises devem ser cacheadas fora do componente ou numa referência estável.

Padrão 1: Cachear no Componente Pai

// ✅ Cria promise no pai, passa pro filho function App() { const [userId, setUserId] = useState(1); const userPromise = useMemo( () => fetchUser(userId), [userId] ); return ( <Suspense fallback={<Spinner />}> <UserProfile userPromise={userPromise} /> </Suspense> ); } function UserProfile({ userPromise }) { const user = use(userPromise); return <div>{user.name}</div>; }

Padrão 2: Usar uma Biblioteca de Dados

A maioria das bibliotecas de data-fetching já lidam com caching:

// ✅ React Query / TanStack Query function UserProfile({ userId }) { const { data: user } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); return <div>{user.name}</div>; } // ✅ SWR com Suspense function UserProfile({ userId }) { const { data: user } = useSWR( `/api/users/${userId}`, fetcher, { suspense: true } ); return <div>{user.name}</div>; }

Padrão 3: Criar um Cache Simples

Pra casos mais simples, você pode criar seu próprio cache:

// Implementação simples de cache const cache = new Map(); function fetchUserCached(userId) { if (!cache.has(userId)) { cache.set(userId, fetchUser(userId)); } return cache.get(userId); } function UserProfile({ userId }) { const user = use(fetchUserCached(userId)); return <div>{user.name}</div>; }

use() com Context: Mais Poderoso que useContext

O use() também pode ler de Context, mas com um superpoder: pode ser chamado condicionalmente.

// ❌ useContext não pode ser condicional function Button({ showTheme }) { // Isso viola as Regras de Hooks if (showTheme) { const theme = useContext(ThemeContext); } } // ✅ use() PODE ser condicional function Button({ showTheme }) { if (showTheme) { const theme = use(ThemeContext); return <button style={{ color: theme.primary }}>Click</button>; } return <button>Click</button>; }

Isso habilita padrões que antes eram impossíveis:

function ConditionalFeature({ featureFlags }) { // Só acessa auth context se a feature requer if (featureFlags.requiresAuth) { const auth = use(AuthContext); if (!auth.user) { return <LoginPrompt />; } } return <Feature />; }

Tratamento de Erros com use()

Quando uma Promise passada pro use() é rejeitada, ela joga um erro. Você precisa de um Error Boundary pra capturar:

function App() { return ( <ErrorBoundary fallback={<ErrorMessage />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

Pra tratamento de erros mais granular, você pode aninhar Error Boundaries:

function Dashboard() { return ( <div className="dashboard"> <ErrorBoundary fallback={<UserError />}> <Suspense fallback={<UserSkeleton />}> <UserWidget /> </Suspense> </ErrorBoundary> <ErrorBoundary fallback={<StatsError />}> <Suspense fallback={<StatsSkeleton />}> <StatsWidget /> </Suspense> </ErrorBoundary> </div> ); }

Criando um Error Boundary Reutilizável

Aqui tá um componente Error Boundary pronto pra produção:

import { Component } from 'react'; class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error }; } componentDidCatch(error, errorInfo) { // Log pro serviço de reporte de erros console.error('Error caught by boundary:', error, errorInfo); } render() { if (this.state.error) { return this.props.fallback ?? ( <div className="error-container"> <h2>Algo deu errado</h2> <button onClick={() => this.setState({ error: null })}> Tentar novamente </button> </div> ); } return this.props.children; } }

Data Fetching em Paralelo

Um dos padrões mais poderosos com use() é o data fetching paralelo. Em vez de requests em cascata, você pode buscar tudo de uma vez:

// ❌ Cascata: Cada fetch espera o anterior function Dashboard({ userId }) { const user = use(fetchUser(userId)); const posts = use(fetchPosts(userId)); // Espera user const comments = use(fetchComments(userId)); // Espera posts return <DashboardView user={user} posts={posts} comments={comments} />; } // ✅ Paralelo: Todos os fetches iniciam simultaneamente function Dashboard({ userId }) { const userPromise = fetchUserCached(userId); const postsPromise = fetchPostsCached(userId); const commentsPromise = fetchCommentsCached(userId); const user = use(userPromise); const posts = use(postsPromise); const comments = use(commentsPromise); return <DashboardView user={user} posts={posts} comments={comments} />; }

A diferença chave: na versão paralela, todas as Promises são criadas antes de qualquer chamada use(). Isso significa que todos os requests começam imediatamente.

Ainda Melhor: Fetch no Route Loader

Pra performance ótima, inicie os fetches o mais cedo possível—idealmente no seu router:

// React Router loader export async function dashboardLoader({ params }) { return { userPromise: fetchUser(params.userId), postsPromise: fetchPosts(params.userId), commentsPromise: fetchComments(params.userId), }; } function Dashboard() { const { userPromise, postsPromise, commentsPromise } = useLoaderData(); const user = use(userPromise); const posts = use(postsPromise); const comments = use(commentsPromise); return <DashboardView user={user} posts={posts} comments={comments} />; }

Isso começa o fetching antes mesmo do componente renderizar!

use() vs Padrões Tradicionais: Uma Comparação

Vamos comparar o use() com outras abordagens de data fetching:

useEffect + useState

// Abordagem tradicional function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); fetchUser(userId) .then(data => !cancelled && setUser(data)) .catch(err => !cancelled && setError(err)) .finally(() => !cancelled && setLoading(false)); return () => { cancelled = true; }; }, [userId]); if (loading) return <Spinner />; if (error) return <Error />; return <div>{user.name}</div>; }

Problemas:

  • Boilerplate verboso
  • Fácil esquecer cleanup
  • Tratamento de race condition é manual
  • Estado loading/error gerenciado em cada componente

Abordagem use()

// Abordagem moderna function UserProfile({ userId }) { const userPromise = useMemo(() => fetchUser(userId), [userId]); const user = use(userPromise); return <div>{user.name}</div>; } // Envolve com Suspense/ErrorBoundary num nível superior function App() { return ( <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

Benefícios:

  • Código de componente limpo e legível
  • Tratamento loading/error separado da lógica de negócio
  • Sem race conditions (React cuida disso)
  • Composável (aninhar boundaries de Suspense pra estados de loading granulares)

Erros Comuns e Como Evitar

Erro 1: Criar Promises Dentro do Componente

// ❌ Cria nova promise a cada render function Bad({ id }) { const data = use(fetch(`/api/${id}`).then(r => r.json())); } // ✅ Cachear a promise const cache = new Map(); function Good({ id }) { if (!cache.has(id)) { cache.set(id, fetch(`/api/${id}`).then(r => r.json())); } const data = use(cache.get(id)); }

Erro 2: Esquecer Suspense Boundary

// ❌ Sem Suspense = crash quando promise tá pending function App() { return <UserProfile userId={1} />; } // ✅ Envolver com Suspense function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> ); }

Erro 3: Esquecer Error Boundary

// ❌ Sem ErrorBoundary = erros não capturados crasham o app function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> ); } // ✅ Adicionar ErrorBoundary function App() { return ( <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

Erro 4: Não Tratar Rejection de Promise Corretamente

// ❌ Fetch pode falhar silenciosamente const promise = fetch('/api/data').then(r => r.json()); // ✅ Tratar erros HTTP const promise = fetch('/api/data').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); });

use() com Server Components

Em React Server Components, o use() fica ainda mais poderoso porque você pode:

  1. Await diretamente em Server Components
  2. Passar Promises pra Client Components
// Server Component async function Page({ params }) { const userPromise = fetchUser(params.id); return ( <Suspense fallback={<Spinner />}> <UserProfile userPromise={userPromise} /> </Suspense> ); } // Client Component 'use client'; function UserProfile({ userPromise }) { const user = use(userPromise); return <div>{user.name}</div>; }

Esse padrão habilita streaming: o servidor envia o shell imediatamente enquanto os dados carregam em background.

Dicas de Otimização de Performance

1. Começar Fetching Cedo

// No seu router ou layout const userPromise = fetchUser(userId); // Passa pro componente <UserProfile userPromise={userPromise} />

2. Usar Streaming com Suspense

function Page() { return ( <> {/* Conteúdo crítico carrega primeiro */} <Header /> {/* Conteúdo não-crítico faz streaming */} <Suspense fallback={<CommentsSkeleton />}> <Comments /> </Suspense> </> ); }

3. Boundaries de Suspense Granulares

function Dashboard() { return ( <div className="grid"> <Suspense fallback={<CardSkeleton />}> <RevenueCard /> </Suspense> <Suspense fallback={<CardSkeleton />}> <UsersCard /> </Suspense> <Suspense fallback={<CardSkeleton />}> <ActivityCard /> </Suspense> </div> ); }

Cada card carrega independentemente—sem esperar o mais lento.

Quando NÃO Usar use()

O use() nem sempre é a escolha certa:

  1. Mutations: Use useTransition ou useActionState pra envios de formulário
  2. Dados em tempo real: Use subscriptions (ex., WebSocket) com useSyncExternalStore
  3. APIs do navegador: Pra localStorage, window size, etc., use hooks apropriados
  4. Estado simples: Pra estado de UI, fique com useState

Guia de Migração: De useEffect pra use()

Aqui tá como migrar código existente:

Passo 1: Identificar useEffects de Data Fetching

Procure padrões como:

useEffect(() => { fetchData().then(setData); }, [dep]);

Passo 2: Extrair pra Promise Cacheada

const cache = new Map(); function fetchDataCached(dep) { const key = JSON.stringify(dep); if (!cache.has(key)) { cache.set(key, fetchData(dep)); } return cache.get(key); }

Passo 3: Substituir por use()

function Component({ dep }) { const data = use(fetchDataCached(dep)); return <View data={data} />; }

Passo 4: Adicionar Suspense e Error Boundaries

<ErrorBoundary fallback={<Error />}> <Suspense fallback={<Loading />}> <Component dep={dep} /> </Suspense> </ErrorBoundary>

Conclusão

O hook use() representa uma mudança de paradigma no desenvolvimento React. Ele nos move de padrões imperativos de "fetch data, depois setState" pra padrões declarativos de "esse componente precisa desses dados".

Pontos chave:

  1. use() lê de Promises e Context com integração Suspense
  2. Promises devem ser cacheadas pra evitar loops infinitos
  3. Suspense cuida de estados de loading, Error Boundaries cuidam de erros
  4. Chamada condicional é permitida, diferente de outros hooks
  5. Comece fetching cedo (em routers/loaders) pra melhor performance
  6. Funciona muito bem com Server Components pra streaming

A curva de aprendizado é real—você precisa pensar diferente sobre fluxo de dados. Mas uma vez que faz sentido, você nunca vai querer voltar pro data fetching com useEffect.

O time do React vem trabalhando pra esse momento há anos. Com o React 19, a visão finalmente se realizou: componentes que simplesmente declaram quais dados precisam, com o React cuidando de toda a complexidade de estados de loading, erros e race conditions.

Bem-vindo ao futuro do data fetching no React.

// O futuro é agora function UserProfile({ userId }) { const user = use(fetchUserCached(userId)); return <div>{user.name}</div>; }

É realmente simples assim.

reactreact-19use-hookdata-fetchingsuspensejavascriptfrontendasync