React Hooks Avançados: Padrões e Otimizações
Domine os React Hooks avançados com padrões testados. Custom hooks, useEffect otimizado, useMemo, useCallback e técnicas de performance.

Os React Hooks revolucionaram a forma de gerenciar estado e efeitos colaterais em componentes funcionais. Além do uso básico de useState e useEffect, padrões avançados permitem criar código reutilizável, performático e manutenível.
Este guia pressupõe familiaridade com os Hooks básicos (useState, useEffect, useContext). Os exemplos utilizam React 18+ e TypeScript para uma tipagem robusta.
Dominando o useEffect: evitando armadilhas comuns
O hook useEffect é frequentemente mal compreendido e mal utilizado. Entender profundamente seu funcionamento ajuda a evitar bugs sutis e problemas de performance.
O princípio fundamental: useEffect sincroniza um componente com um sistema externo. Se o efeito não se comunica com o mundo exterior (API, DOM, timer), provavelmente é desnecessário.
// Custom hook to synchronize document title
import { useEffect } from 'react'
export function useDocumentTitle(title: string) {
useEffect(() => {
// Save the previous title
const previousTitle = document.title
// Update the title
document.title = title
// Cleanup: restore previous title on unmount
return () => {
document.title = previousTitle
}
}, [title]) // Only trigger when title changes
}O erro mais comum é omitir o array de dependências ou incluir valores instáveis. Todo valor utilizado dentro do efeito precisa estar listado nas dependências.
'use client'
import { useEffect, useState } from 'react'
interface User {
id: string
name: string
email: string
}
interface UserProfileProps {
userId: string
}
export function UserProfile({ userId }: UserProfileProps) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// Flag to prevent state updates after unmount
let isMounted = true
// Controller to cancel pending requests
const controller = new AbortController()
async function fetchUser() {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error('User not found')
}
const data = await response.json()
// Check if component is still mounted
if (isMounted) {
setUser(data)
}
} catch (err) {
// Ignore abort errors
if (err instanceof Error && err.name !== 'AbortError') {
if (isMounted) {
setError(err.message)
}
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchUser()
// Cleanup: abort request and mark as unmounted
return () => {
isMounted = false
controller.abort()
}
}, [userId]) // Re-run effect when userId changes
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return null
return (
<div className="p-4 border rounded-lg">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
)
}Esse padrão com AbortController e a flag isMounted previne vazamentos de memória e atualizações de estado em componentes desmontados.
Criando custom hooks reutilizáveis
Os custom hooks encapsulam lógica reutilizável. Um bom custom hook segue o princípio da responsabilidade única e expõe uma API clara.
// Generic hook to persist state in localStorage
import { useState, useEffect, useCallback } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
// Initialize state with stored value or default
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage "${key}":`, error)
return initialValue
}
})
// Sync with localStorage on every change
useEffect(() => {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(key, JSON.stringify(storedValue))
} catch (error) {
console.warn(`Error writing localStorage "${key}":`, error)
}
}, [key, storedValue])
// Function to update value
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const nextValue = value instanceof Function ? value(prev) : value
return nextValue
})
}, [])
// Function to remove value
const removeValue = useCallback(() => {
setStoredValue(initialValue)
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key)
}
}, [key, initialValue])
return [storedValue, setValue, removeValue]
}Esse hook pode ser utilizado em qualquer componente para persistir dados.
'use client'
import { useLocalStorage } from '@/hooks/useLocalStorage'
type Theme = 'light' | 'dark' | 'system'
export function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system')
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
className="px-3 py-2 border rounded-lg"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
)
}Os custom hooks sempre começam com "use" (useLocalStorage, useFetch, useDebounce). Essa convenção permite ao React verificar as regras dos hooks e às ferramentas de linting funcionarem corretamente.
Padrão de composição com useReducer
Para estados complexos com múltiplas ações possíveis, o useReducer oferece uma estrutura mais previsível do que várias chamadas de useState.
// Cart management hook with useReducer
import { useReducer, useCallback, useMemo } from 'react'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
isOpen: boolean
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'TOGGLE_CART' }
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(
(item) => item.id === action.payload.id
)
if (existingItem) {
// Increment quantity if item exists
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
}
// Add new item
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
}
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload)
}
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter((item) => item.quantity > 0)
}
case 'CLEAR_CART':
return { ...state, items: [] }
case 'TOGGLE_CART':
return { ...state, isOpen: !state.isOpen }
default:
return state
}
}
const initialState: CartState = {
items: [],
isOpen: false
}
export function useCart() {
const [state, dispatch] = useReducer(cartReducer, initialState)
// Memoized actions to avoid re-renders
const addItem = useCallback(
(item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item })
},
[]
)
const removeItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id })
}, [])
const updateQuantity = useCallback((id: string, quantity: number) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
}, [])
const clearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' })
}, [])
const toggleCart = useCallback(() => {
dispatch({ type: 'TOGGLE_CART' })
}, [])
// Memoized derived values
const total = useMemo(
() => state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
[state.items]
)
const itemCount = useMemo(
() => state.items.reduce((sum, item) => sum + item.quantity, 0),
[state.items]
)
return {
items: state.items,
isOpen: state.isOpen,
total,
itemCount,
addItem,
removeItem,
updateQuantity,
clearCart,
toggleCart
}
}Essa abordagem centraliza toda a lógica do carrinho e facilita os testes unitários.
Pronto para mandar bem nas entrevistas de React / Next.js?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Otimização com useMemo e useCallback
Esses hooks evitam cálculos ou recriações de funções desnecessárias. Seu uso deve ser direcionado: aplicá-los em todo lugar degrada a performance em vez de melhorá-la.
'use client'
import { useState, useMemo, useCallback, memo } from 'react'
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
interface ProductListProps {
products: Product[]
}
// Memoized child component
const ProductCard = memo(function ProductCard({
product,
onAddToCart
}: {
product: Product
onAddToCart: (id: string) => void
}) {
console.log(`Render ProductCard: ${product.name}`)
return (
<div className="border rounded-lg p-4">
<h3 className="font-medium">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
<button
onClick={() => onAddToCart(product.id)}
disabled={!product.inStock}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
)
})
export function ProductList({ products }: ProductListProps) {
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
const [cart, setCart] = useState<string[]>([])
// useMemo: memoize result of expensive computation
const filteredAndSortedProducts = useMemo(() => {
console.log('Computing filteredAndSortedProducts')
let result = products
// Filter by name
if (filter) {
result = result.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
)
}
// Sort
result = [...result].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
return a.price - b.price
})
return result
}, [products, filter, sortBy]) // Recompute only when these deps change
// useCallback: memoize a function
const handleAddToCart = useCallback((productId: string) => {
setCart((prev) => [...prev, productId])
}, []) // Empty deps: function never changes
// Memoized statistics
const stats = useMemo(
() => ({
total: filteredAndSortedProducts.length,
inStock: filteredAndSortedProducts.filter((p) => p.inStock).length,
avgPrice:
filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) /
filteredAndSortedProducts.length || 0
}),
[filteredAndSortedProducts]
)
return (
<div>
<div className="mb-4 flex gap-4">
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search..."
className="px-3 py-2 border rounded-lg"
/>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}
className="px-3 py-2 border rounded-lg"
>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</div>
<p className="text-sm text-gray-600 mb-4">
{stats.total} products ({stats.inStock} in stock) -
Average price: ${stats.avgPrice.toFixed(2)}
</p>
<div className="grid grid-cols-3 gap-4">
{filteredAndSortedProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
</div>
)
}Esses hooks adicionam complexidade. Devem ser reservados para: (1) cálculos genuinamente custosos, (2) props passadas a componentes memoizados com memo(), (3) dependências de outros hooks.
Hook de debounce para campos de busca
O debounce evita que uma ação seja executada com frequência excessiva, tipicamente durante a digitação em um campo de busca.
// Generic debounce hook
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
// Create a timer that updates the value after delay
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cleanup: cancel timer if value changes before delay
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}'use client'
import { useState, useEffect } from 'react'
import { useDebounce } from '@/hooks/useDebounce'
interface SearchResult {
id: string
title: string
}
export function SearchInput() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
// Debounce the query by 300ms
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
// Don't search if query is empty
if (!debouncedQuery.trim()) {
setResults([])
return
}
async function search() {
setLoading(true)
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(debouncedQuery)}`
)
const data = await response.json()
setResults(data.results)
} catch (error) {
console.error('Search error:', error)
} finally {
setLoading(false)
}
}
search()
}, [debouncedQuery]) // Only triggered when debouncedQuery changes
return (
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full px-4 py-2 border rounded-lg"
/>
{loading && (
<div className="absolute right-3 top-2.5">
<div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
)}
{results.length > 0 && (
<ul className="absolute w-full mt-1 bg-white border rounded-lg shadow-lg">
{results.map((result) => (
<li
key={result.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
{result.title}
</li>
))}
</ul>
)}
</div>
)
}O componente só executa a busca 300 ms depois que o usuário para de digitar, evitando dezenas de requisições inúteis.
useImperativeHandle para refs avançadas
Esse hook expõe métodos específicos de um componente filho ao pai por meio de uma ref.
'use client'
import {
useRef,
useImperativeHandle,
forwardRef,
useState,
useCallback
} from 'react'
// Interface of exposed methods
export interface VideoPlayerRef {
play: () => void
pause: () => void
seek: (time: number) => void
getCurrentTime: () => number
}
interface VideoPlayerProps {
src: string
poster?: string
}
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
function VideoPlayer({ src, poster }, ref) {
const videoRef = useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
// Expose only desired methods to parent
useImperativeHandle(
ref,
() => ({
play: () => {
videoRef.current?.play()
setIsPlaying(true)
},
pause: () => {
videoRef.current?.pause()
setIsPlaying(false)
},
seek: (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time
}
},
getCurrentTime: () => {
return videoRef.current?.currentTime ?? 0
}
}),
[] // No deps: methods always use current ref
)
const handlePlayPause = useCallback(() => {
if (isPlaying) {
videoRef.current?.pause()
} else {
videoRef.current?.play()
}
setIsPlaying(!isPlaying)
}, [isPlaying])
return (
<div className="relative">
<video
ref={videoRef}
src={src}
poster={poster}
className="w-full rounded-lg"
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
<button
onClick={handlePlayPause}
className="absolute bottom-4 left-4 px-4 py-2 bg-black/50 text-white rounded"
>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
)
}
)'use client'
import { useRef } from 'react'
import { VideoPlayer, VideoPlayerRef } from './VideoPlayer'
export function VideoController() {
const playerRef = useRef<VideoPlayerRef>(null)
const handleSkipForward = () => {
if (playerRef.current) {
const currentTime = playerRef.current.getCurrentTime()
playerRef.current.seek(currentTime + 10)
}
}
return (
<div className="space-y-4">
<VideoPlayer
ref={playerRef}
src="/videos/demo.mp4"
poster="/images/poster.jpg"
/>
<div className="flex gap-2">
<button
onClick={() => playerRef.current?.play()}
className="px-4 py-2 bg-green-600 text-white rounded"
>
Play
</button>
<button
onClick={() => playerRef.current?.pause()}
className="px-4 py-2 bg-red-600 text-white rounded"
>
Pause
</button>
<button
onClick={handleSkipForward}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
+10s
</button>
</div>
</div>
)
}Esse padrão é especialmente útil para componentes de mídia, formulários ou qualquer interface que exija controle imperativo.
Hook para gerenciar o ciclo de vida de requisições
Um hook avançado para lidar com os estados de carregamento, erro e dados com tipagem estrita.
// Generic hook for HTTP requests
import { useState, useEffect, useCallback, useRef } from 'react'
interface UseFetchState<T> {
data: T | null
loading: boolean
error: Error | null
}
interface UseFetchOptions {
immediate?: boolean // Execute immediately on mount
onSuccess?: <T>(data: T) => void
onError?: (error: Error) => void
}
export function useFetch<T>(
url: string,
options: UseFetchOptions = {}
) {
const { immediate = true, onSuccess, onError } = options
const [state, setState] = useState<UseFetchState<T>>({
data: null,
loading: immediate,
error: null
})
// Ref to prevent updates after unmount
const mountedRef = useRef(true)
// Ref for abort controller
const abortControllerRef = useRef<AbortController | null>(null)
const execute = useCallback(async () => {
// Abort previous request if in progress
abortControllerRef.current?.abort()
abortControllerRef.current = new AbortController()
setState((prev) => ({ ...prev, loading: true, error: null }))
try {
const response = await fetch(url, {
signal: abortControllerRef.current.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data: T = await response.json()
if (mountedRef.current) {
setState({ data, loading: false, error: null })
onSuccess?.(data)
}
return data
} catch (error) {
// Ignore abort errors
if (error instanceof Error && error.name === 'AbortError') {
return null
}
const err = error instanceof Error ? error : new Error('Unknown error')
if (mountedRef.current) {
setState({ data: null, loading: false, error: err })
onError?.(err)
}
return null
}
}, [url, onSuccess, onError])
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null })
}, [])
// Execute on mount if immediate=true
useEffect(() => {
if (immediate) {
execute()
}
return () => {
mountedRef.current = false
abortControllerRef.current?.abort()
}
}, [execute, immediate])
return {
...state,
execute,
reset,
isIdle: !state.loading && !state.data && !state.error
}
}Esse hook fornece uma API completa para tratar qualquer requisição com cancelamento automático e gerenciamento de ciclo de vida.
Pronto para mandar bem nas entrevistas de React / Next.js?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Conclusão
Os padrões avançados de React Hooks permitem produzir código manutenível e performático. Pontos-chave para lembrar:
- ✅ useEffect: sempre incluir cleanup e tratar race conditions com AbortController
- ✅ Custom hooks: encapsular lógica reutilizável com uma API clara
- ✅ useReducer: preferir para estados complexos com múltiplas ações
- ✅ useMemo/useCallback: usar de forma direcionada, apenas quando necessário
- ✅ useDebounce: limitar execuções frequentes em buscas
- ✅ useImperativeHandle: expor métodos imperativos de maneira controlada
Dominar esses padrões diferencia o desenvolvedor React intermediário do experiente. Cada hook resolve um problema específico: escolher a ferramenta certa no momento certo é a chave de uma arquitetura React sólida.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

React Compiler em 2026: memoização automática e perguntas de entrevista
Análise completa do React Compiler em 2026: pipeline de compilação, memoização automática, regras do React e perguntas frequentes em entrevistas técnicas.

React 19: Server Components em producao - O guia completo
Dominar os Server Components do React 19 em producao. Arquitetura, padroes, streaming, caching e otimizacoes para aplicacoes de alta performance.

React Server Components em produção: padrões e armadilhas
React Server Components em produção: padrões testados em batalha, anti-padrões comuns e estratégias de depuração para aplicações Next.js 15 robustas.