React 19 foi oficialmente lançado, adicionando muitas novas funcionalidades incluindo Server Actions, novos hooks e melhorias significativas no processamento de formulários. Neste artigo, explicamos as principais novas funcionalidades do React 19 com exemplos práticos de código.
Principais Novas Funcionalidades do React 19
Visão Geral
flowchart TB
subgraph React19["React 19"]
subgraph Hooks["Novos Hooks"]
H1["use() - Leitura de Promise/Context"]
H2["useActionState() - Estado de ação de formulário"]
H3["useFormStatus() - Estado de envio de formulário"]
H4["useOptimistic() - Atualização otimista"]
end
subgraph Actions["Actions"]
A1["Server Actions - Funções server-side"]
A2["Client Actions - Processamento assíncrono no cliente"]
A3["Integração de Formulário - form action"]
end
subgraph Others["Outras Melhorias"]
O1["ref as prop - forwardRef desnecessário"]
O2["Document Metadata - escrita direta de title etc."]
O3["Gerenciamento de Stylesheet - controle de ordem com precedence"]
O4["Resource Preloading - API prefetch/preload"]
end
end
Hook use()
Leitura de Promise
use() é um novo hook para ler Promises ou Context durante a renderização.
// use() - Leitura de Promise
import { use, Suspense } from 'react';
// Função de busca de dados
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Componente
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
// Usar combinado com Suspense
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Componente pai
function UserPage({ userId }: { userId: string }) {
// Passar Promise como props
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<div>Carregando usuário...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// use() pode ser chamado condicionalmente (diferença de outros hooks)
function ConditionalData({ shouldFetch, dataPromise }: {
shouldFetch: boolean;
dataPromise: Promise<Data>;
}) {
if (!shouldFetch) {
return <div>Dados não necessários</div>;
}
// Pode ser usado após ramificação condicional
const data = use(dataPromise);
return <div>{data.value}</div>;
}
Leitura de Context
// use() - Leitura de Context
import { use, createContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function ThemedButton() {
// use() pode ser usado em vez de useContext()
const theme = use(ThemeContext);
return (
<button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
Clique aqui
</button>
);
}
// Leitura condicional de Context
function ConditionalTheme({ useTheme }: { useTheme: boolean }) {
if (!useTheme) {
return <button>Botão Padrão</button>;
}
// Pode ser usado após ramificação condicional
const theme = use(ThemeContext);
return <button className={`theme-${theme}`}>Botão com Tema</button>;
}
Actions
Server Actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Server Action
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validação
if (!title || title.length < 3) {
return { error: 'O título deve ter pelo menos 3 caracteres' };
}
// Salvar no banco de dados
const post = await db.post.create({
data: { title, content },
});
// Revalidar cache
revalidatePath('/posts');
// Redirecionar
redirect(`/posts/${post.id}`);
}
// Action de atualização
export async function updatePost(id: string, formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.update({
where: { id },
data: { title, content },
});
revalidatePath(`/posts/${id}`);
return { success: true };
}
// Action de exclusão
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/posts');
redirect('/posts');
}
Integração com Formulários
// app/posts/new/page.tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions';
export default function NewPostPage() {
// useActionState - Gerenciamento de estado de ação de formulário
const [state, formAction, isPending] = useActionState(
createPost,
{ error: null }
);
return (
<form action={formAction}>
<div>
<label htmlFor="title">Título</label>
<input
id="title"
name="title"
required
disabled={isPending}
/>
</div>
<div>
<label htmlFor="content">Conteúdo</label>
<textarea
id="content"
name="content"
disabled={isPending}
/>
</div>
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Publicando...' : 'Publicar'}
</button>
</form>
);
}
useFormStatus
// Obter estado de envio de formulário
import { useFormStatus } from 'react-dom';
function SubmitButton() {
// Obtém estado de envio do <form> pai
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<>
<Spinner />
Enviando...
</>
) : (
'Enviar'
)}
</button>
);
}
// Uso em formulário
function ContactForm() {
async function submitForm(formData: FormData) {
'use server';
// Processamento de envio
}
return (
<form action={submitForm}>
<input name="email" type="email" required />
<textarea name="message" required />
{/* SubmitButton obtém automaticamente o estado do form pai */}
<SubmitButton />
</form>
);
}
useOptimistic - Atualização Otimista
// Implementação de atualização otimista
import { useOptimistic, startTransition } from 'react';
interface Message {
id: string;
text: string;
sending?: boolean;
}
function ChatMessages({ messages }: { messages: Message[] }) {
// Gerenciamento de estado otimista
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: Message) => [
...state,
{ ...newMessage, sending: true },
]
);
async function sendMessage(formData: FormData) {
const text = formData.get('message') as string;
const tempId = crypto.randomUUID();
// Adicionar otimisticamente
startTransition(() => {
addOptimisticMessage({
id: tempId,
text,
sending: true,
});
});
// Enviar para o servidor
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({ text }),
});
}
return (
<div>
<ul>
{optimisticMessages.map((message) => (
<li
key={message.id}
className={message.sending ? 'opacity-50' : ''}
>
{message.text}
{message.sending && <span> (Enviando...)</span>}
</li>
))}
</ul>
<form action={sendMessage}>
<input name="message" required />
<button type="submit">Enviar</button>
</form>
</div>
);
}
Exemplo de Botão de Curtir
// Botão de curtir otimista
function LikeButton({ postId, initialLiked, initialCount }: {
postId: string;
initialLiked: boolean;
initialCount: number;
}) {
const [{ liked, count }, setOptimistic] = useOptimistic(
{ liked: initialLiked, count: initialCount },
(state, newLiked: boolean) => ({
liked: newLiked,
count: state.count + (newLiked ? 1 : -1),
})
);
async function toggleLike() {
const newLiked = !liked;
// Atualizar otimisticamente
startTransition(() => {
setOptimistic(newLiked);
});
// Enviar para o servidor
await fetch(`/api/posts/${postId}/like`, {
method: newLiked ? 'POST' : 'DELETE',
});
}
return (
<button onClick={toggleLike}>
{liked ? '❤️' : '🤍'} {count}
</button>
);
}
ref as prop
No React 19, você pode receber ref como props sem forwardRef.
// React 18 e anteriores: forwardRef necessário
const InputOld = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// React 19: ref pode ser passado como prop normal
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// Exemplo de uso
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<form>
<Input ref={inputRef} placeholder="Digite o texto" />
<button
type="button"
onClick={() => inputRef.current?.focus()}
>
Focar
</button>
</form>
);
}
Document Metadata
Agora você pode escrever metadados diretamente dentro de componentes.
// Escrita direta de metadados
function BlogPost({ post }: { post: Post }) {
return (
<article>
{/* Automaticamente hoisted para o head do documento */}
<title>{post.title} - Meu Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// Uso em múltiplas páginas
function ProductPage({ product }: { product: Product }) {
return (
<div>
<title>{product.name} | Minha Loja</title>
<meta name="description" content={product.description} />
{/* Dados estruturados */}
<script type="application/ld+json">
{JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
price: product.price,
})}
</script>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
Gerenciamento de Stylesheet
// Gerenciamento de prioridade de stylesheet
function ComponentWithStyles() {
return (
<>
{/* Controle de ordem de carregamento com precedence */}
<link
rel="stylesheet"
href="/styles/base.css"
precedence="default"
/>
<link
rel="stylesheet"
href="/styles/components.css"
precedence="default"
/>
<link
rel="stylesheet"
href="/styles/utilities.css"
precedence="high"
/>
<div className="styled-component">
Conteúdo
</div>
</>
);
}
// Stylesheet dinâmico
function ThemeSwitcher({ theme }: { theme: 'light' | 'dark' }) {
return (
<>
<link
rel="stylesheet"
href={`/themes/${theme}.css`}
precedence="high"
/>
<div>Conteúdo com tema</div>
</>
);
}
Resource Preloading
// API de pré-carregamento de recursos
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
function ResourceHints() {
// Pré-resolução de DNS
prefetchDNS('https://api.example.com');
// Pré-conexão
preconnect('https://cdn.example.com');
// Pré-carregamento de recurso
preload('/fonts/custom.woff2', {
as: 'font',
type: 'font/woff2',
crossOrigin: 'anonymous',
});
// Pré-inicialização de script
preinit('/scripts/analytics.js', {
as: 'script',
});
return <div>Conteúdo</div>;
}
// Pré-carregamento de imagens
function ImageGallery({ images }: { images: string[] }) {
// Pré-carregar próximas imagens
useEffect(() => {
images.slice(1, 4).forEach((src) => {
preload(src, { as: 'image' });
});
}, [images]);
return (
<div>
{images.map((src) => (
<img key={src} src={src} alt="" />
))}
</div>
);
}
React Compiler (Experimental)
// Memoização automática pelo React Compiler
// Antes: useMemo/useCallback manual necessário
function ProductListOld({ products, onSelect }: Props) {
const sortedProducts = useMemo(
() => [...products].sort((a, b) => a.price - b.price),
[products]
);
const handleSelect = useCallback(
(id: string) => onSelect(id),
[onSelect]
);
return (
<ul>
{sortedProducts.map((product) => (
<ProductItem
key={product.id}
product={product}
onSelect={handleSelect}
/>
))}
</ul>
);
}
// Depois: React Compiler memoriza automaticamente
function ProductList({ products, onSelect }: Props) {
// Memoização manual desnecessária - compilador otimiza
const sortedProducts = [...products].sort((a, b) => a.price - b.price);
return (
<ul>
{sortedProducts.map((product) => (
<ProductItem
key={product.id}
product={product}
onSelect={(id) => onSelect(id)}
/>
))}
</ul>
);
}
// Habilitar React Compiler em babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Opções
}],
],
};
Melhorias no Tratamento de Erros
// Exibição de erros melhorada
// Exibição detalhada de erros de hydration
// No React 19, diferenças são mostradas especificamente
// Melhoria do Error Boundary
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
// React 19: Stack trace mais detalhado
console.error('Erro:', error);
console.error('Component Stack:', info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Exemplo de uso
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<MainContent />
</ErrorBoundary>
);
}
Guia de Migração
Migração do React 18 para 19
# Atualização de pacotes
npm install react@19 react-dom@19
# Definições de tipos TypeScript
npm install -D @types/react@19 @types/react-dom@19
// Principais mudanças
// 1. forwardRef → prop normal
// Antes
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input ref={ref} {...props} />
));
// Depois
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// 2. useContext → use (opcional)
// Antes
const theme = useContext(ThemeContext);
// Depois (conveniente para uso condicional)
const theme = use(ThemeContext);
// 3. Remoção de APIs deprecadas
// - defaultProps (componentes de função)
// - propTypes
// - createFactory
// - render (react-dom)
// Alternativa para defaultProps
// Antes
function Button({ size = 'medium' }) { ... }
Button.defaultProps = { size: 'medium' };
// Depois (usar parâmetro padrão)
function Button({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
// ...
}
Resumo
React 19 oferece muitas novas funcionalidades que melhoram significativamente a experiência do desenvolvedor.
Principais Novas Funcionalidades
| Funcionalidade | Uso |
|---|---|
| use() | Leitura de Promise/Context |
| useActionState | Gerenciamento de estado de ação de formulário |
| useFormStatus | Obter estado de envio |
| useOptimistic | Atualização otimista |
| Server Actions | Processamento server-side |
| ref as prop | forwardRef desnecessário |
Recomendações de Migração
- Novos projetos: Adotar React 19
- Projetos existentes: Migrar gradualmente
- Server Actions: Combinar com Next.js 14+
React 19 permite um desenvolvimento de aplicações React mais intuitivo e rápido.