Criando useStableCallback (useEvent): callbacks estáveis no React
Por John Pires · 13 de março de 2026

Introducao ao problema
Callback instável em React é uma fonte comum de dor para quem começa. Novos programadores veem uma funcao criada dentro do componente e assumem que ela e barata. Na pratica, identidade de funcao importa quando voce passa callbacks para filhos memoizados, hooks que dependem de referencias ou APIs que comparam por igualdade referencial.
Imagine uma chave de porta que muda a cada vez que voce entra em casa. O cadeado funciona, mas qualquer sistema que memorize a chave falha. O mesmo acontece se um callback mudar a cada render: memorization, comparacoes por referencia e otimizacoes quebram.
Dois erros aparecem com frequencia em code reviews: passar funcoes inline para componentes memoizados sem motivo, e tentar resolver usando useCallback com dependencia errada, criando callbacks que ficam desatualizados ou que ainda recriam quando nao deveriam.
Este artigo mostra o jeito errado, explica por que falha, e entao apresenta um padrao prático: um hook useStableCallback (tambem conhecido como polyfill de useEvent) que entrega uma funcao com identidade estável e sempre invoca a versão mais recente do handler.
Pre-requisitos e setup
O que voce precisa saber
Conceitos basicos de React: componentes funcionais, hooks useState e useEffect, JSX. Entender memoizacao com React.memo e por que a identidade de props importa ajuda a absorver as implicacoes. Nao precisa ser expert em performance.
Tambem e util conhecer useRef e useLayoutEffect, porque a implementacao segura do hook usa esses blocos para manter a referencia atualizada sem provocar renders.
Como preparar o projeto
Crie um app com Create React App, Vite ou similar. Exemplo rapido com Vite:
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
Abra o projeto e substitua o conteudo do App por exemplos abaixo. Todo codigo apresentado funciona em React 18. Para simplicidade, usamos apenas hooks do core e nao dependencias externas.
Tutorial guiado passo a passo
Vamos fazer code review: primeiro o jeito errado que muitos iniciantes usam, depois os problemas e por fim a solucao com uso do hook useStableCallback.
1) Jeito errado: funcoes inline e filhos memoizados
Exemplo comum: um componente pai com contador passa um onClick inline para um botao memoizado. A intenção e evitar re-renders no filho, mas o callback inline recria a cada render do pai, forcando o filho a re-renderizar se ele comparar por referencia.
// O que faz: component App cria handler inline e passa para Child memoizado
// O que acontece: Child re-renderiza a cada vez porque recepciona uma nova funcao
import React, { useState } from 'react'
const Child = React.memo(function Child({ onAction }) {
console.log('Child render')
return Click me
})
export default function App() {
const [count, setCount] = useState(0)
// handler inline re-criado em cada render
return (
count: {count} setCount(c => c + 1)} />
)
}Problema: mesmo que Child seja memoizado, a prop onAction tem nova identidade em cada render, quebrando a memoizacao. Resultado: re-renders desnecessarios e menos performace.
2) Tentativa: useCallback com dependencia errada
Muitos querem resolver com useCallback. Isso reduz recriacao quando dependencias nao mudam, mas se voce incluir o estado errado ou omitir dependencias, pode ter callbacks que ficam com closures antigas.
// O que faz: usa useCallback para manter a identidade, mas captura estado de forma errada
// O que acontece: handler nao atualiza o estado corretamente quando omitimos dependencias
import React, { useState, useCallback } from 'react'
function Child({ onAction }) {
console.log('Child render')
return Click me
}
export default function App() {
const [count, setCount] = useState(0)
// tentativa: memorizar handler, mas se omitirmos count, o handler tera closure antigo
const increment = useCallback(() => {
// se dependencias estao erradas, isto usa uma versao antiga de count
setCount(count + 1)
}, [])
return (
count: {count}
)
}Problema: ou voce inclui count nas dependencias e o callback passa a recriar, ou omite e tem closure estatico. Nao existe magica com useCallback; depende do que voce passa.
3) Solucao: useStableCallback (padrao useEvent)
Ideia: criar uma funcao cuja identidade e constante entre renders, mas que sempre encaminha a chamada para a ultima versao do handler. Assim, filhos memoizados veem a mesma referencia e nao re-renderizam, enquanto o handler usa o estado mais recente.
Implementamos useStableCallback usando useRef para armazenar o handler atual e useLayoutEffect para manter o ref sincronizado e evitar rasgos de render.
// O que faz: hook que retorna uma funcao estável que chama a versão mais recente do handler
// O que acontece: identidade permanece igual entre renders, handler sempre atual
import { useLayoutEffect, useRef } from 'react'
export function useStableCallback(handler) {
const handlerRef = useRef(handler)
useLayoutEffect(() => {
handlerRef.current = handler
})
const stableRef = useRef()
if (!stableRef.current) {
stableRef.current = function(...args) {
return handlerRef.current && handlerRef.current.apply(this, args)
}
}
return stableRef.current
}
Explicacao tecnica: useLayoutEffect atualiza handlerRef antes do navegador pintar, garantindo que, se o callback for chamado durante efeitos ou mesmo eventos fora do ciclo de render, a referencia esteja atualizada. A funcao real de retorno e criada apenas uma vez; sua implementacao sempre delega para handlerRef.current.
Resultado final funcionando
Exemplo completo: contador com Child memoizado
Agora aplicamos o hook no App. O Child e memoizado e nao re-renderiza quando o pai muda o estado, porque onAction tem refererencia estável. Ao mesmo tempo, o clique usa o estado mais recente.
// O que faz: App usa useStableCallback e Child memoizado
// O que acontece: Child nao re-renderiza por causa da prop estável, contador atualiza corretamente
import React, { useState } from 'react'
import { useStableCallback } from './useStableCallback'
const Child = React.memo(function Child({ onAction }) {
console.log('Child render')
return Add
})
export default function App() {
const [count, setCount] = useState(0)
const onAdd = useStableCallback(() => {
// nao importa quando esta funcao foi registrada, ela acessa a ultima versao
setCount(c => c + 1)
})
return (
count: {count}
)
}Se voce abrir o console, vera que Child renderiza apenas uma vez na primeira montagem. Depois, cada clique incrementa o contador sem re-renderizar o filho apenas por causa do callback.
Por que preferir useLayoutEffect ao useEffect aqui
useLayoutEffect roda sincronamente apos a renderizacao mas antes da pintura. Atualizar handlerRef ali evita que, em casos limite, um efeito que leia a referencia logo apos o render veja uma versao antiga. Para a maioria dos eventos de DOM e invocacoes sincrono, isso torna o comportamento previsivel.
Erros comuns, armadilhas e dicas praticas
A seguir, uma lista de problemas que aparecem em code reviews e como evita los.
Ignorar identidade vs fechamento: useCallback protege identidade, mas nao resolve fechamento de variaveis. Entenda se precisa da ultima versao do estado ou apenas da mesma referencia.
Atualizar refs no useEffect: usar useEffect pode deixar uma janela onde handlerRef aponta para versao anterior; useLayoutEffect e aceitavel quando precisa de coherencia imediata.
Delegar errado this ou args: na implementacao usamos apply(this, args) para preservar comportamentos caso o handler dependa do contexto ou dos argumentos dinâmicos.
Usar para tudo sem medir: padrao ajuda quando se passa callbacks para componentes memoizados ou hooks que comparam por referencia. Nao e preciso para todo setState simples.
Se um filho memoizado re-renderiza, investigue primeiro quais props mudam por identidade. Callback inline costuma ser o culpado mais frequente.
Dica de debug: coloque console.log em Child e use React DevTools para inspecionar props. Quando a prop da funcao muda, a ferramenta mostra nova referencia.
Desafio para o leitor praticar
Para fixar o conceito, proponho dois exercicios práticos, do mais simples ao intermediario.
Refatore um componente que passa callbacks para varios filhos memoizados. Antes, cada filho re-renderiza em qualquer mudanca; depois, aplique useStableCallback para evitar re-renders desnecessarios.
Implemente um pequeno formulario com validacao em tempo real e um componente de sugestoes memoizado. Use useStableCallback para passar funcoes de manipulacao para as sugestoes sem quebrar a atualizacao do input.
Para checar se voce fez certo: o numero de renders dos filhos deve cair, mas o comportamento da aplicacao deve continuar correto e atualizado. Use console.log e React DevTools para confirmar.
Conclusao pratica
Callbacks estaveis resolvem um padrao recorrente: manter identidade sem sacrificar acesso ao estado mais recente. O useStableCallback mostrado entrega essa propriedade com poucas linhas e sem depender de APIs experimentais.
Resumo das principais ideias:
Identidade referencial importa para memoizacao e comparacoes por igualdade.
useCallback controla identidade, mas nao evita stale closures se usado incorretamente.
useStableCallback combina referencia atualizada e funcao de retorno constante, resolvendo ambos os problemas.
Implementar e aplicar esse padrao e uma boa pratica de performance desde os primeiros projetos: elimina re-renders silenciosos e reduz a necessidade de adicionar dependencias complexas em muitos useEffect.
Se quiser, entregue o exercicio e eu faco um code review apontando re-renders desnecessarios e possibilidades de melhor otimizar o uso do hook.
Assuntos: React, hooks, Performance, JavaScript, patterns
Leia também
Melhorar LCP com React Server Components e Suspense: streaming, placeholders e revalidação
Reduza LCP com React Server Components e Suspense: prioridades de dados, streaming incremental, placeholders que preservam layout e revalidação de cache…
Paginação e infinite scroll com React Server Components, Suspense e streaming SSR
Pattern prático para pagination e infinite scroll com React Server Components, Suspense e streaming SSR: mostro o jeito errado, depois o certo, com exemplos…
Sincronizando estado entre abas com BroadcastChannel e useSyncExternalStore
Aprenda a sincronizar estado entre abas no browser usando BroadcastChannel e useSyncExternalStore do React. Guia passo a passo, exemplo funcional de contador…
useEffect vs useLayoutEffect no React: quando usar, exemplos e armadilhas
Compare useEffect e useLayoutEffect com exemplos práticos: quando usar cada um, medição de DOM sem layout shift, armadilhas comuns e padrões seguros para…
Suavizando transições em listas React com startTransition, useDeferredValue e Suspense
Como evitar travamentos em listas React: use startTransition para priorizar atualizações, useDeferredValue para adiar derivação de valores, e Suspense para…
Sincronizar estado entre Server e Client Components com use e useSyncExternalStore
Padronize estado entre Server e Client Components usando use (servidor) e useSyncExternalStore (cliente). Evite fetch duplicado, hydrate sem mismatch e…