Lançamento Oficial do React 19 - Actions, use() e Novos Hooks Completos

2025.12.02

React 19.0 TypeScript 5.5+
Documentação Oficial

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

FuncionalidadeUso
use()Leitura de Promise/Context
useActionStateGerenciamento de estado de ação de formulário
useFormStatusObter estado de envio
useOptimisticAtualização otimista
Server ActionsProcessamento server-side
ref as propforwardRef 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.

← Voltar para a lista