useImperativeHandle
Como personalizar el tipo de ref que reciben nuestros componentes con forwardRef desde React 16.8

Desarrollando código desde los 80s desde que en los 80 vi por primera vez un Amstrad en un escaparate de una tienda de electrónica en mi ciudad natal, lo que me llevó a insistirles a mis padres para que me apuntaran a clases de informática donde aprendí cosas como Basic y ensamblador del x86.
A lo largo de mi carrera profesional he pasado por varias revoluciones (y crisis) que han hecho que me haya tenido que ir reinventando para poder seguir desarrollando mi trabajo de la mejor forma posible.
Creo que el desarrollo de software tiene una parte de ciencia pero también una de artesanía (y creatividad) que lo convierten en algo apasionante.
Eternamente contagiado con el Síndrome del Impostor.
En este artículo vamos a explicar el hook useImperativeHandle y para ello nos vamos a apoyar en el ejemplo de una aplicación muy sencilla en la que se mostrará un contador y dos botones que nos van a servir para incrementar o decrementar el valor del mismo.

El código del componente que se encargará de renderizar este contador es algo tan sencillo como lo siguiente donde simplemente estamos haciendo uso de una variable de estado count para guardar el valor de nuestro contador y definimos dos funciones que serán invocadas para incrementar y decrementar su valor:
import { useState } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const decrement = () => {
setCount(count - 1)
}
const increment = () => {
setCount(count + 1)
}
return (
<div>
<h1 className='text-2xl'>Count: {count}</h1>
<button onClick={decrement}>Decrement</button>
<button onClick={increment}>Increment</button>
</div>
)
}
export default Counter
Supongamos además que queremos definir una nueva función a la que vamos a denorminar reset que, como su nombre indica, lo que viene a hacer es resetear el valor de contador para que pase a ser cero pero además de alguna manera lo que queremos hacer es que esta función que estará definida en nuestro componente queremos que pueda ser utilizada por cualquier componente padre que use a nuestro Counter.
const reset = () => {
setCounter(0)
}
Para establecer mejor la base de código de nuestro ejemplo vamos a mostrar el código del componente padre que estará utilizando nuestro Counter para poder tener todo el escenario que queremos resolver a la vista:
import Counter from './Counter'
export const Page = () => (
<div className='mb-2'>
<Counter />
</div>
)
Nota: es importante entender que el ejemplo que estamos desarrollando no se corresponde con la forma en la que deberíamos pensar a la hora de desarrollar nuestros componentes en React puesto que las piezas de estado que son compartidas por varios componentes de nuestra aplicación (en nueste ejemplo la variable
counter) deberían ser situadas en aquellos componentes dentro de la jerarquía de componentes de React que fuese más accesible para todos los que la vayan a usar (en nuestro caso en el componentePage) pero es cierto que es un caso de uso de se nos podría presentar en nuestro día a día.Algunos ejemplos de situaciones en las que se nos podrían plantear casos como el que estamos definiendo tendrían que ver con componentes en los que es realmente complicado manejar el estado fuera de ellos (y por lo tanto no podemos pasarlo a los componentes padres en la jerarquía de componentes) o bien cuando estamos usando librerías de terceros y no tenemos acceso a las funciones que pueda haber dentro de ellas para controlar el estado de los componentes.
useImperativeHandle
Ahora que conocemos el escenario al que nos enfrentamos es cuando vamos a ver que el hook useImperativeHandle es el ideal para solucionarlo siendo este un hook que nos proporciona React con el fin de poder exponer parte de la funcionalidad de nuestros componentes a sus padres haciendo uso de una ref.
Lo primero que lo que tenemos que pensar es que como useImperativeHandle se basa en el uso de un ref para lograr su objetivo lo primero que vamos a tener que hacer es conseguir que nuestro componente Counter acepte una ref como parte de sus props. La forma de lograrlo es gracias a la invocación de la función forwardRef que también nos proporciona React por lo que lo primero que vamos a hacer es importarla:
import { forwardRef, useState } from 'react'
¿Qué tenemos que hacer ahora? Pues en lo que tenemos que saber es que forwardRef es una función que acepta como parámetro un componente de React por lo que como export que estamos realizando en nuestro código lo que exportaremos será el resultado de la invocación de esta función:
export default forwardRef(Counter)
Simplemente con esto lo que hemos logrado es que nuestro Counter pueda aceptar una ref como parámetro. Ahora bien, lo que tenemos que recordar es que el React los componentes no son más que funciones que aceptan como parámetro un objeto que representa a sus props, pero además es posible que reciban más parámetros y es en ello en lo que nos vamos a basar en el caso que estamos resolviendo.
Así pues, lo primero que hacemos es definir las props que acepta nuestro componente Counter (que en este caso no sería ninguna puesto que no acepta ninguna pero es necesario que las definamos para poder establecer el primero de los parámetros de la definición de nuestro componente):
type CounterProps = {}
Aplicando este tipo en la definición de nuestro componente escribiremos algo como:
const Counter = (props: CounterProps) => {
Y ahora podemos establecer la ref que será el segundo parámetro de la invocación. Como estamos trabajando con TypeScript vamos a necesitar importar el tipo que establece una ref y que nos proporciona React:
import { forwardRef, Ref, useState } from 'react'
Sabiendo que este tipo Ref es un genérico de TypeScript por ahora lo que vamos a hacer es establecerlo a any a la hora de definir el segundo de los parámetros de la definición de nuestro componente:
const Counter = (props: CounterProps, ref: Ref<any>) => {
Vamos ahora a definir la función que queremos exponer hacia afuera (hacia los padres del componente) dentro del código del mismo puesto que hasta ahora no lo había hecho:
const Counter = (props: CounterProps, ref: Ref<any>) => {
//...
const reset = () => {
setCounter(0)
}
Ahora que ya tenemos montado todo el esqueleto de partida para la utilización de useImperativeHandle como siempre lo primero que vamos a tener que hacer es importarlo:
import { forwardRef, Ref, useImperativeHandle, useState } from 'react'
De tal manera que en el código de nuestro componente tras la definición de la función reset vamos a poder pasar a hacer uso del mismo sabiendo que este recibe como primer parámetro la ref que nuestro componente está recibiendo como parte de sus props gracias al uso de forwardRef y el segundo paráemetro será una función que ha de permitir el acceso a las propiedades de nuestro componente que nosotros queramos gracias a que ha de retornar un objeto que permitirá este interacción. Por lo tanto la definición del hook sería algo como:
useImperativeHandle(ref, () => ({}))
Nota: esta es la forma genérica en la que se utilizará
useImperativeHandleen una aplicación de React de tal manera que todos los componentes padres deCounterque proporcionen una ref van a tener acceso a todos los atributos y funciones que sean devueltos en el objeto que retorna la función que forma el segundo parámetro del hook.
Sabiendo esto, lo que ahora tenemos que hacer es proporcionar el código de nuestra función reset como uno de los métodos que estarán asociados a objeto que retorna useImperativeHandle. Como estamos trabajando con TypeScript vamo a definir el tipo que estará asociado a la ref que retornará el hook que en nuestro caso será el tipo que define a nuestra función reset el cual además exportaremos para que cualquier componente padre tenga acceso a su definición y lo pueda usar de forma correcta:
export type CounterRef = {
reset: () => void
}
Ahora en al definición de la Ref dentro del componente ya podemos quitar el any puesto que sabemos cuál será exactamente el tipo de referencia que Counter va a proporcionar a sus padres por lo que cambiamos su definición como sigue:
const Counter = (props: CounterProps, ref: Ref<CounterRef>) => {
Dicho de otra manera, lo que estamos haciendo es decir que
Countersolamente va a aceptar refs que sean del tipoCounterRef.
Nos quedará establecer de forma correcta el objeto que retorna useImperativeHandle para que TypeScript no nos muestre un error y es que la ref que se ha creado (el objeto que retorna este hook) deberá tener el método reset:
useImperativeHandle(ref, () => ({ reset }))
Con todas las modificaciones que acabmos de realizar y con el fin de tenerlo todo agrupado en un mismo sitio vamos a mostrar el código completo del componente Counter:
import { forwardRef, Ref, useState } from 'react'
export type CounterRef = {
reset: () => void
}
type CounterProps = {}
const Counter = (props: CounterProps, ref: Ref<CounterRef>) => {
const [count, setCount] = useState(0)
const decrement = () => {
setCount(count - 1)
}
const increment = () => {
setCount(count + 1)
}
const reset = () => {
setCounter(0)
}
useImperativeHandle(ref, () => ({ reset }))
return (
<div>
<h1 className='text-2xl'>Count: {count}</h1>
<button onClick={decrement}>Decrement</button>
<button onClick={increment}>Increment</button>
</div>
)
}
export default forwardRef(Counter)
Con todas estas modificaciones que hemos realizado tendremos la garantía que cualquier padre de Counter que lo llame proporcionando una ref del tipo CounterRef tendrá a su disposición el acceso al método reset que permitirá reestablecer el valor de nuestro contador.
Invocación desde el padre
Vamos a ver cómo podemos llamar a reset desde el padre que en nuestro caso es el componente Page. Para ello lo primero que tendremos que hacer dentro del mismo es crear una ref que sea del tipo CounterRef y para ello nos tenemos que apoyar en el uso del hook useRef que nos proporciona React diciéndole que el tipo de esta ref será CounterRef y además que inicialmente tiene el valor null puesto que no estará establecido a ningún valor.
import { useRef } from 'react'
import Counter, { CounterRef } from './Counter'
export const Page = () => {
const counterRef = useRef<CounterRef>(null)
Hecho esto el siguiente paso será pasar esta ref como parte de las props que recibe el componente Counter:
<Counter ref={counterRef} />
Y nos quedará crear un nuevo elemento dentro de la interfaz de usuario que nos permita usar esta ref siempre y cuando haya sido inicializada (es decir, que el atributo current de la ref no sea null) para invocar el método reset:
<button onClick={() => ref.current?.reset()}>Reset from parent</button>
El código completo del componente Page tras recoger todas las modificaciones que acabamos de mencionar será algo parecido a lo que podemos ver a continuación:
import { useRef } from 'react'
import Counter, { CounterRef } from './Counter'
export const Page = () => {
const counterRef = useRef<CounterRef>(null)
return (
<>
<div className='mb-2'>
<Counter ref={counterRef} />
</div>
<button onClick={() => ref.current?.reset()}>Reset from parent</button>
<>
)
}
Si ahora nos vamos a la interfaz de usuario veremos como ha aparecido un nuevo botón dentro de la misma y si incrementamos o decrementamos el valor del contador y posteriormente pulsamos sobre el botón Reset from parent lo que logramos es precisamente establecer el valor de contador nuevamente a cero pero desde el componente padre tal y como queríamos.

Y este es realmente el comportamiento que hace especialmente poderoso a este hook cuando es necesario aplicarlo. De hecho, los métodos que se exporten a través de la ref no tienen porque ser tan sencillos como el que acabamos de definir en nuestro ejemplo. Pensemos en que podríamos definir algo como lo siguiente:
export type CounterRef = {
checkSubscribed: (value: boolean) => void,
reset: () => void
}
Y ahora cuando estemos definiendo el objeto que retorna useImperativeHandle lo que tendremos es que especificarlo lo que implica definir el código que tendrá asociado pudiendo hacer algo como lo siguiente:
useImperativeHandle(ref, () => ({
checkSubscribed: value => console.log(value),
reset
}))
Otra forma de declarar una forwardRef
Es posible que en la base de código con la que estemos trabajando no se esté haciendo uso de los export default como en el ejemplo que acabamos de ver pudiendo ser que nuestro componente Counter esté declarado como sigue:
export const Counter = () => {
¿Qué pasos tendremos que usar para poder usar la función forwardRef? Pues básicamente los mismos que hemos visto anteriormente puesto que el problema al que nos podremos enfrentar es que no seamos capaces de conocer la sintaxis.
Empezaríamos definiendo las props que aceptará nuestro componente y asignándolas como el parámetro que recibe la función que permite que sea un componente de React:
type CounterProps = {}
export const Counter = (props: CounterProps) => {
Ahora definiremos el tipo de datos que está asociado a la ref que esperamos que se reciba:
export type CounterRef = {
reset: () => void
}
y ya simplemente lo que hacemos será invocar a la función forwardRef como sigue obteniendo el mismo resultado que hemos explicado anteriormente:
export const Counter = forwardRef(props: CounterProps, ref: CounterRef) => {
Trabajando con elementos HTML
Vamos a centrarnos en un caso especial en el uso de useImperativeHandle que tiene que ver con el uso de las ref de forma local en un componente en el que lo vamos a utilizar. Para entenderlo mejor vamos a partir nuevamente de un ejemplo:
import { forwardRef, Ref, useImperativeHandle, useRef} from 'react';
interface TextInputProps {}
export interface TextInputRef {
reset: () => void;
}
export const TextInput = (props: TextInputProps, ref: Ref<TextInputRef>) => {
const localRef = useRef<HTMLInputRef>(null)
useImperativeHandle(ref, () => ({
reset: () => {
if (!localRef.current) return
localRef.current.value = ''
localRef.current.focus()
}
}))
return <input type='text' ref={localRef} />
}
export default forwardRef(TextInput)
Lo que estamos mostrando en el ejemplo anterior es un caso un poquito más complejo del que habíamos mostrado en primero lugar donde el valor del contador se estaba guardando dentro de un atributo del estado del componente Counter. En este caso el valor del contador estará dentro de un elemento input de HTML y el problema aquí es que el componente padre no tiene acceso a ese elemento mediante la ref del tipo CounterRef ya que solamente tiene acceso al método reset que esta ref le proporiciona.
¿Cuál es la solución aquí? Pues como podemos ver en el código que hemos mostrado tendremos que crear una ref del tipo HTMLInputElement (puesto que el lugar donde estamos guardando el valor del contador es un elemento input y este es su tipo correcto) siendo esta la ref que pasaremos al elemento input cuando lo estamos creando como un atributo del mismo y no la ref que nos ha proporcionado el componente padre.
Ahora como la función reset que está retornando useImperativeHandle está definida dentro del propio componente va a tener acceso a esta localRef y por lo tanto utilizarla dentro de la lógica que sea necesario implementar para lograr el comportamiento que deseemos.
¿Cómo aplicamos este nuevo componente en un elemento padre? Pues siguiendo la misma lógica que hemos descrito hasta ahora. Así podemos definir algo como lo siguiente:
import { useRef } from 'react'
import TextInput, { TextInputRef } from './TextInput'
export const Page2 = () => {
const inputRef = useRef<TextInputRef>(null)
return (
<>
<div className='mb-2'>
<TextInput ref={inputRef} />
</div>
<button onClick={() => inputRef.current?.reset()}>Reset from parent</button>
</>
)
}
Ahora podemos ver que el comportamiento de esta nuevo componente Page2 nos dejaría algo parecido a lo que podemos ver en la siguiente imagen:





