Destino Dev

Blog

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.

  1. 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.

  2. 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