Back

React & TypeScript: Padrões Comuns para Código Mais Limpo

React & TypeScript: Padrões Comuns para Código Mais Limpo

Ao construir aplicações React com TypeScript, você encontrará os mesmos desafios repetidamente: como tipar props corretamente, lidar com eventos de forma segura e estruturar componentes para máxima reutilização. Esses padrões não são apenas sobre satisfazer o compilador—são sobre escrever código que é mais fácil de entender, refatorar e manter.

Este artigo cobre os padrões React TypeScript mais práticos que você usará diariamente. Você aprenderá como tipar componentes efetivamente, lidar com props opcionais, trabalhar com eventos e refs, aproveitar utility types e usar discriminated unions em reducers. Cada padrão inclui exemplos focados mostrando exatamente por que ele melhora seu código.

Pontos-Chave

  • Tipar props e state explicitamente usando interfaces ou types para melhor IntelliSense e detecção de erros
  • Lidar com props opcionais e padrão com propriedades opcionais do TypeScript e parâmetros padrão
  • Usar utility types como Pick e ComponentProps para reduzir repetição e manter consistência
  • Tipar eventos e refs adequadamente para interações DOM type-safe
  • Aproveitar discriminated unions em reducers para tratamento exaustivo de ações
  • Compor componentes com forwardRef para manter type safety ao passar refs

Tipando Props e State

A base dos padrões React TypeScript começa com a tipagem adequada das props do seu componente. Isso captura erros em tempo de compilação e fornece melhor autocomplete no seu editor.

Padrão Básico de Props

interface ButtonProps {
  label: string
  onClick: () => void
  disabled?: boolean
}

function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  )
}

Este padrão garante que seus componentes recebam os tipos de dados corretos. A interface documenta claramente quais props o componente espera, tornando-o autodocumentado.

Tipando State com useState

O TypeScript geralmente infere tipos de state corretamente, mas states complexos se beneficiam de tipagem explícita:

import { useState } from 'react'

interface User {
  id: string
  name: string
  email: string
}

function UserProfile() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(false) // Tipo inferido como boolean

  // TypeScript sabe que user pode ser null
  return user ? <div>{user.name}</div> : <div>Carregando...</div>
}

Lidando com Props Opcionais e Padrão

Componentes React frequentemente têm props de configuração opcionais. As propriedades opcionais do TypeScript combinadas com parâmetros padrão criam um padrão limpo:

interface CardProps {
  title: string
  description?: string
  variant?: 'primary' | 'secondary'
  className?: string
}

function Card({ 
  title, 
  description, 
  variant = 'primary',
  className = ''
}: CardProps) {
  return (
    <div className={`card card-${variant} ${className}`}>
      <h2>{title}</h2>
      {description && <p>{description}</p>}
    </div>
  )
}

Este padrão torna os componentes flexíveis mantendo type safety. Props obrigatórias são claras, e props opcionais têm padrões sensatos.

Tipando Eventos e Refs

Event handlers e refs são fontes comuns de erros TypeScript. Estes padrões eliminam adivinhações:

Padrão de Event Handler

import { useState, ChangeEvent, FormEvent } from 'react'

interface FormProps {
  onSubmit: (data: { email: string; password: string }) => void
}

function LoginForm({ onSubmit }: FormProps) {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value)
  }

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    onSubmit({ email, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={handleEmailChange} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Entrar</button>
    </form>
  )
}

Padrão de Ref

import { useRef, useEffect } from 'react'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    // TypeScript sabe que inputRef.current pode ser null
    inputRef.current?.focus()
  }, [])

  return <input ref={inputRef} type="text" />
}

Usando Utility Types

Os utility types do TypeScript reduzem duplicação de código e mantêm tipos sincronizados em toda sua aplicação.

Padrão ComponentProps

Extraia props de componentes existentes sem repetir definições de tipo:

import { ComponentProps } from 'react'

// Obtenha todas as props que um elemento button aceita
type ButtonProps = ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary'
}

function StyledButton({ variant = 'primary', ...props }: ButtonProps) {
  return <button className={`btn-${variant}`} {...props} />
}

// Extraia props dos seus próprios componentes
type MyButtonProps = ComponentProps<typeof StyledButton>

Padrão Pick

Selecione props específicas ao estender componentes:

interface UserData {
  id: string
  name: string
  email: string
  role: string
  lastLogin: Date
}

// Só precisa de algumas propriedades para o card
type UserCardProps = Pick<UserData, 'name' | 'email'> & {
  onClick?: () => void
}

function UserCard({ name, email, onClick }: UserCardProps) {
  return (
    <div onClick={onClick}>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

Compondo Componentes com forwardRef

Ao construir bibliotecas de componentes ou envolver elementos nativos, forwardRef mantém type safety enquanto expõe refs:

import { forwardRef, useRef, InputHTMLAttributes } from 'react'

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string
  error?: string
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => {
    return (
      <div>
        <label>{label}</label>
        <input ref={ref} {...props} />
        {error && <span className="error">{error}</span>}
      </div>
    )
  }
)

Input.displayName = 'Input'

// Uso mantém type safety completa
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)
  
  return <Input ref={inputRef} label="Email" type="email" />
}

Discriminated Unions em Reducers

Discriminated unions tornam ações de reducer type-safe e habilitam verificação exaustiva:

interface TodoState {
  items: Array<{ id: string; text: string; done: boolean }>
  filter: 'all' | 'active' | 'completed'
}

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string }
  | { type: 'SET_FILTER'; payload: TodoState['filter'] }
  | { type: 'CLEAR_COMPLETED' }

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        items: [...state.items, {
          id: Date.now().toString(),
          text: action.payload,
          done: false
        }]
      }
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, done: !item.done }
            : item
        )
      }
    
    case 'SET_FILTER':
      return { ...state, filter: action.payload }
    
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        items: state.items.filter(item => !item.done)
      }
    
    default:
      // TypeScript garante que todos os casos sejam tratados
      const exhaustive: never = action
      return state
  }
}

Este padrão garante que você trate todos os tipos de ação. Se você adicionar um novo tipo de ação, o TypeScript dará erro até que você o trate no reducer.

Atualizações do React 19

Com o React 19, novas capacidades como Actions (useActionState, useFormStatus), a API use() para consumir promises ou context, e suporte de primeira classe para Server Components tornam type safety ainda mais importante. Ao tipar Actions, defina explicitamente a forma do FormData e o valor de retorno da ação para que tanto código servidor quanto cliente permaneçam sincronizados. A mudança futura de forwardRef para passar ref como uma prop padrão também simplifica a tipagem—trate ref como qualquer outra prop e deixe o TypeScript garantir correção através dos limites dos componentes.

Conclusão

Estes padrões React TypeScript comuns formam a base de aplicações React type-safe. Ao tipar props e state explicitamente, lidar com eventos e refs corretamente, e aproveitar os utility types do TypeScript, você cria código que é tanto mais seguro quanto mais sustentável. Os padrões funcionam juntos—tipagem adequada de props permite melhor composição de componentes, enquanto discriminated unions garantem tratamento exaustivo em lógica de state complexa.

Comece com tipagem básica de props e gradualmente incorpore utility types e padrões avançados conforme sua aplicação cresce. O investimento em tipagem adequada se paga através de melhor suporte da IDE, menos erros em runtime e contratos de componente mais claros.

Perguntas Frequentes

Use interface quando você pode precisar estender ou mesclar declarações depois. Use type para unions, intersections ou ao trabalhar com utility types. Ambos funcionam bem para props básicas, então escolha uma abordagem e mantenha consistência.

Use ReactNode para máxima flexibilidade: children: ReactNode. Isso aceita strings, números, elementos, fragments e arrays. Para casos mais específicos, use ReactElement ou tipos de elementos específicos.

Use funções genéricas que preservam os tipos de prop do componente original enquanto adicionam novos. O utility type ComponentProps ajuda a extrair e estender tipos de componentes existentes sem repetição.

Tipe parâmetros de função, tipos de retorno para funções exportadas e state complexo explicitamente. Deixe o TypeScript inferir casos simples como useState com primitivos ou event handlers óbvios para reduzir verbosidade.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers