Como Usar useImperativeHandle e forwardRef: Expondo APIs de Componentes em Produção Já leu

O Problema: Componentes como Caixas Pretas Em React, componentes funcionais são frequentemente tratados como "caixas pretas" — você passa props e recebe JSX. No entanto, existem cenários onde você precisa acessar diretamente a lógica interna ou métodos de um componente filho a partir do componente pai. Por exemplo, você pode querer chamar um método que foca um input, reproduz um vídeo, ou valida um formulário sem re-renderizar toda a aplicação. O React fornece o Hooks junto com justamente para resolver esse problema. Eles permitem que você exponha uma API imperativa (métodos e valores) de um componente funcional para que o componente pai possa acessá-la. Isso quebra o padrão reativo normal de React, então deve ser usado com cuidado, apenas quando absolutamente necessário. Entendendo forwardRef: Acessando Refs do Filho O que é uma Ref? Uma Ref (referência) em React é um objeto que armazena uma referência persistente a um nó DOM ou a uma instância de componente. Diferente de props, Refs

O Problema: Componentes como Caixas Pretas

Em React, componentes funcionais são frequentemente tratados como "caixas pretas" — você passa props e recebe JSX. No entanto, existem cenários onde você precisa acessar diretamente a lógica interna ou métodos de um componente filho a partir do componente pai. Por exemplo, você pode querer chamar um método que foca um input, reproduz um vídeo, ou valida um formulário sem re-renderizar toda a aplicação.

O React fornece o Hooks useImperativeHandle junto com forwardRef justamente para resolver esse problema. Eles permitem que você exponha uma API imperativa (métodos e valores) de um componente funcional para que o componente pai possa acessá-la. Isso quebra o padrão reativo normal de React, então deve ser usado com cuidado, apenas quando absolutamente necessário.

Entendendo forwardRef: Acessando Refs do Filho

O que é uma Ref?

Uma Ref (referência) em React é um objeto que armazena uma referência persistente a um nó DOM ou a uma instância de componente. Diferente de props, Refs não disparam re-renderizações quando mudam. Você cria uma Ref usando useRef ou createRef.

import { useRef } from 'react';

export default function InputForm() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>Focar no Input</button>
    </>
  );
}

Neste exemplo simples, inputRef.current nos dá acesso direto ao elemento DOM do input, permitindo chamar focus().

O Problema com Componentes Funcionais

Quando você tenta passar uma ref diretamente a um componente funcional, React ignora a prop ref por padrão. Componentes funcionais não têm instâncias como classes tinham, então você não pode referenciar um componente funcional de forma padrão.

// ❌ Isso NÃO funciona
function MeuComponente() {
  return <input type="text" />;
}

const App = () => {
  const ref = useRef(null);
  return <MeuComponente ref={ref} />;  // ref será undefined
};

É aqui que entra forwardRef. Ele permite que um componente funcional "encaminhe" a ref recebida para um elemento filho, ou melhor ainda, para exposições imperativas que você definir com useImperativeHandle.

Usando forwardRef

forwardRef envolve seu componente funcional e permite receber a prop ref como segundo argumento. O componente recebe props como primeiro argumento e ref como segundo.

import { forwardRef, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} type="text" placeholder={props.placeholder} />;
});

CustomInput.displayName = 'CustomInput';

export default function App() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <CustomInput ref={inputRef} placeholder="Digite algo..." />
      <button onClick={handleFocus}>Focar</button>
    </>
  );
}

Aqui, o componente CustomInput encaminha a ref recebida diretamente para o elemento <input>. Dessa forma, o componente pai consegue acessar o nó DOM do input e chamar métodos como focus(), blur(), etc.

Expondo uma API com useImperativeHandle

A Motivação: Métodos Customizados

Enquanto forwardRef permite expor elementos DOM, useImperativeHandle permite expor uma API customizada — uma interface imperativa que você define. Ao invés de apenas expor o nó DOM bruto, você pode criar métodos que encapsulam lógica do componente.

Considere um componente de formulário validado. O pai poderia querer chamar um método validate() sem acessar diretamente os inputs internos. Ou um componente de vídeo que expõe métodos como play(), pause() e getCurrentTime().

Sintaxe e Conceito

useImperativeHandle é um Hook que permite customizar o objeto que é exposto quando uma ref é acessada. Você o utiliza dentro do componente que será "refenciado", junto com forwardRef.

import { forwardRef, useImperativeHandle, useRef, useState } from 'react';

const ValidatedForm = forwardRef((props, ref) => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [errors, setErrors] = useState({});

  useImperativeHandle(ref, () => ({
    validate: () => {
      const newErrors = {};

      if (!name.trim()) {
        newErrors.name = 'Nome é obrigatório';
      }

      if (!email.includes('@')) {
        newErrors.email = 'Email inválido';
      }

      setErrors(newErrors);
      return Object.keys(newErrors).length === 0;
    },

    getValues: () => ({ name, email }),

    reset: () => {
      setName('');
      setEmail('');
      setErrors({});
    }
  }), [name, email]);

  return (
    <div>
      <div>
        <label>Nome:</label>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        {errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}
      </div>

      <div>
        <label>Email:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        {errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
      </div>
    </div>
  );
});

ValidatedForm.displayName = 'ValidatedForm';

export default function App() {
  const formRef = useRef(null);

  const handleSubmit = () => {
    if (formRef.current.validate()) {
      const values = formRef.current.getValues();
      console.log('Formulário válido:', values);
    } else {
      console.log('Formulário contém erros');
    }
  };

  const handleReset = () => {
    formRef.current.reset();
  };

  return (
    <div>
      <ValidatedForm ref={formRef} />
      <button onClick={handleSubmit}>Enviar</button>
      <button onClick={handleReset}>Limpar</button>
    </div>
  );
}

Neste exemplo, o componente ValidatedForm expõe três métodos: validate(), getValues() e reset(). O componente pai não precisa conhecer os detalhes internos (estados, elementos DOM específicos); apenas chama esses métodos quando necessário.

Dependency Array: Uma Armadilha Comum

Observe que useImperativeHandle recebe um dependency array como terceiro argumento. Se você incluir valores que mudam frequentemente (como name e email no exemplo acima), o objeto inteiro será recriado a cada render, causando refs instáveis.

// ❌ Problema: Objeto da API recriado a cada mudança de name/email
useImperativeHandle(ref, () => ({
  validate: () => { /* ... */ },
  getValues: () => ({ name, email }),
  reset: () => { /* ... */ }
}), [name, email]);  // ← Dependency array problemático

Na maioria dos casos, você quer que a API seja estável, então o dependency array fica vazio []. Se seus métodos precisam acessar estado atual, use funções que capturam o estado no momento da chamada:

// ✅ Melhor: Dependências vazias, métodos acessam estado atual
useImperativeHandle(ref, () => ({
  getValues: () => ({ name, email }),  // Captura estado atual quando chamado
  validate: () => { /* ... */ }
}), []);

Casos de Uso Reais e Boas Práticas

Quando Usar useImperativeHandle

Use useImperativeHandle apenas quando a abordagem reativa (props, callbacks, estado gerenciado no pai) não for adequada. Exemplos legítimos incluem:

  • Focar um input ou textarea após uma ação específica
  • Controlar mídia (vídeo, áudio) — play, pause, seek
  • Validar um formulário complexo e retornar resultado
  • Ativar animações programaticamente
  • Gerenciar estado imperativo que o pai precisa controlar diretamente
import { forwardRef, useImperativeHandle, useRef } from 'react';

const VideoPlayer = forwardRef((props, ref) => {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current.play(),
    pause: () => videoRef.current.pause(),
    setTime: (seconds) => {
      videoRef.current.currentTime = seconds;
    },
    getCurrentTime: () => videoRef.current.currentTime,
    getDuration: () => videoRef.current.duration
  }), []);

  return (
    <video
      ref={videoRef}
      src={props.src}
      width={props.width}
      height={props.height}
    />
  );
});

VideoPlayer.displayName = 'VideoPlayer';

export default function App() {
  const playerRef = useRef(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="video.mp4" width={400} height={300} />
      <button onClick={() => playerRef.current.play()}>Play</button>
      <button onClick={() => playerRef.current.pause()}>Pause</button>
      <button onClick={() => playerRef.current.setTime(10)}>Ir para 10s</button>
      <p>Tempo atual: {playerRef.current?.getCurrentTime() || 0}s</p>
    </div>
  );
}

O que Evitar

Não use useImperativeHandle como um atalho para ignorar o fluxo reativo de dados em React. Se você pode resolver algo com props, callbacks ou estado compartilhado, use isso. Imperatives devem ser exceção, não regra.

// ❌ Evite: Lógica de negócio complexa exposta imperativamente
useImperativeHandle(ref, () => ({
  updateUserData: async (id) => { /* fetch e lógica */ },
  calculateTaxes: (amount) => { /* complexa lógica financeira */ }
}), []);

// ✅ Prefira: Passar esses dados como props ou callbacks
<ComponenteFilho
  userData={userData}
  onUserUpdate={handleUserUpdate}
  taxes={calculatedTaxes}
/>

Conclusão

Primeiro aprendizado: forwardRef e useImperativeHandle são ferramentas para quebrar o padrão reativo do React de forma controlada. forwardRef permite que componentes funcionais recebam e encaminhem refs, enquanto useImperativeHandle permite expor uma API customizada — métodos e valores — que o componente pai pode chamar imperativamente.

Segundo aprendizado: Essas APIs resolvem problemas específicos onde o fluxo reativo (props, estado, callbacks) não é suficiente ou é impraticável — como controlar mídia, focar inputs ou validar formulários complexos. Use-as com moderação e sempre questione se existe uma abordagem mais "reativa" para o seu problema.

Terceiro aprendizado: O dependency array em useImperativeHandle deve ser vazio na maioria dos casos para manter a API estável. Se seus métodos precisam acessar estado atual, deixe-os capturarem esse estado no momento da execução, não durante a criação do objeto da API.

Referências

  1. React Documentation - forwardRef
  2. React Documentation - useImperativeHandle
  3. Overreacted - Making setInterval Declarative with Hooks
  4. React Patterns - Ref Forwarding
  5. JavaScript.info - refs in React

Artigos relacionados