En el desarrollo moderno de React, el gancho useImperativeHandle es una forma poderosa de personalizar el valor expuesto de un componente y brinda más control sobre sus métodos y propiedades internas. Como resultado, las API de componentes más eficientes pueden mejorar la flexibilidad y la capacidad de mantenimiento del producto.
En este artículo, el equipo de Social Discovery Group analiza en profundidad las mejores prácticas para utilizar useImperativeHandle de manera eficaz para mejorar los componentes de React.
React proporciona muchos ganchos (la documentación oficial describe 17 ganchos al momento de escribir este artículo) para administrar el estado, los efectos y las interacciones entre componentes.
Entre ellos, useImperativeHandle es una herramienta útil para crear una interfaz programática (API) para componentes secundarios, que se agregó a React desde la versión 16.8.0 en adelante.
useImperativeHandle le permite personalizar lo que devolverá la referencia pasada a un componente. Funciona en conjunto con forwardRef, que permite pasar una referencia a un componente secundario.
useImperativeHandle(ref, createHandle, [deps]);
Este gancho permite el control externo del comportamiento de un componente, lo que puede ser útil en determinadas situaciones, como trabajar con bibliotecas de terceros, animaciones complejas o componentes que requieren acceso directo a métodos. Sin embargo, debe usarse con precaución, ya que rompe el enfoque declarativo de React.
Imaginemos que necesitamos manipular el DOM de un componente secundario. A continuación, se muestra un ejemplo de cómo hacerlo mediante una referencia.
import React, { forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { // Use forwardRef to pass the ref to the input element return <input ref={ref} {...props} />; }); export default function App() { const inputRef = useRef(); const handleFocus = () => { inputRef.current.focus(); // Directly controlling the input }; const handleClear = () => { inputRef.current.value = ''; // Directly controlling the input value }; return ( <div> <CustomInput ref={inputRef} /> <button onClick={handleFocus}>Focus</button> <button onClick={handleClear}>Clear</button> </div> ); }
Y aquí está cómo se puede lograr usando useImperativeHandle.
import React, { useImperativeHandle, forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, clear: () => { inputRef.current.value = ''; }, })); return <input ref={inputRef} {...props} />; }); export default function App() { const inputRef = useRef(); return ( <div> <CustomInput ref={inputRef} /> <button onClick={inputRef.current.focus}>Focus</button> <button onClick={inputRef.current.clear}>Clear</button> </div> ); }
Como se ve en los ejemplos anteriores, al utilizar useImperativeHandle, el componente secundario proporciona al componente principal un conjunto de métodos que definimos nosotros mismos.
El uso de useImperativeHandle en escenarios avanzados, como en ejemplos con animación, permite aislar comportamientos complejos dentro de un componente. Esto hace que el componente principal sea más simple y legible, especialmente cuando se trabaja con bibliotecas de animación o sonido.
import React, { useRef, useState, useImperativeHandle, forwardRef, memo } from "react"; import { Player } from '@lottiefiles/react-lottie-player' import animation from "./animation.json"; const AnimationWithSound = memo( forwardRef((props, ref) => { const [isAnimating, setIsAnimating] = useState(false); const animationRef = useRef(null); const targetDivRef = useRef(null); useImperativeHandle( ref, () => ({ startAnimation: () => { setIsAnimating(true); animationRef.current?.play() updateStyles("start"); }, stopAnimation: () => { animationRef.current?.stop() updateStyles("stop"); }, }), [] ); const updateStyles = (action) => { if (typeof window === 'undefined' || !targetDivRef.current) return; if (action === "start") { if (targetDivRef.current.classList.contains(styles.stop)) { targetDivRef.current.classList.remove(styles.stop); } targetDivRef.current.classList.add(styles.start); } else if (action === "stop") { if (targetDivRef.current.classList.contains(styles.start)) { targetDivRef.current.classList.remove(styles.start); } targetDivRef.current.classList.add(styles.stop); } }; return ( <div> <Player src={animation} loop={isAnimating} autoplay={false} style={{width: 200, height: 200}} ref={animationRef} /> <div ref={targetDivRef} className="target-div"> This div changes styles </div> </div> ); }) ); export default function App() { const animationRef = useRef(); const handleStart = () => { animationRef.current.startAnimation(); }; const handleStop = () => { animationRef.current.stopAnimation(); }; return ( <div> <h1>Lottie Animation with Sound</h1> <AnimationWithSound ref={animationRef} /> <button onClick={handleStart}>Start Animation</button> <button onClick={handleStop}>Stop Animation</button> </div> ); }
En este ejemplo, el componente secundario devuelve los métodos startAnimation y stopAnimation, que encapsulan una lógica compleja dentro de sí mismos.
El error no siempre se nota de inmediato. Por ejemplo, el componente principal puede cambiar frecuentemente las propiedades y es posible que se produzca una situación en la que se siga utilizando un método obsoleto (con datos obsoletos).
Ejemplo de error:
https://use-imperative-handle.levkovich.dev/deps-is-not-correct/wrong
const [count, setCount] = useState(0); const increment = useCallback(() => { console.log("Current count in increment:", count); // Shows old value setCount(count + 1); // Are using the old value of count }, [count]); useImperativeHandle( ref, () => ({ increment, // Link to the old function is used }), [] // Array of dependencies do not include increment function );
El enfoque correcto:
const [count, setCount] = useState(0); useImperativeHandle( ref, () => ({ increment, }), [increment] // Array of dependencies include increment function );
2. Falta la matriz de dependencia
Si no se proporciona la matriz de dependencias, React asumirá que el objeto en useImperativeHandle debe volver a crearse en cada renderización. Esto puede causar problemas de rendimiento importantes, especialmente si se realizan cálculos "pesados" dentro del gancho.
Ejemplo de error:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }); // Array of dependencies is missing
El enfoque correcto:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct
La modificación directa de ref.current altera el comportamiento de React. Si React intenta actualizar la referencia, puede provocar conflictos o errores inesperados.
Ejemplo de error:
useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });
El enfoque correcto:
useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));
Llamar a métodos proporcionados a través de useImperativeHandle desde useEffect o controladores de eventos, asumiendo que la referencia ya está disponible, puede generar errores: siempre verifique la corriente actual antes de llamar a sus métodos.
Ejemplo de error:
const increment = useCallback(() => { childRef.current.increment(); }, [])
El enfoque correcto:
const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])
Si useImperativeHandle devuelve métodos que cambian de estado sincrónicamente (por ejemplo, iniciar una animación y modificar estilos simultáneamente), puede provocar una "brecha" entre el estado visual y la lógica interna. Asegúrese de que haya coherencia entre el estado y el comportamiento visual, por ejemplo, mediante el uso de efectos (useEffect).
Ejemplo de error:
useImperativeHandle(ref, () => ({ startAnimation: () => { setState("running"); // Animation starts before the state changes lottieRef.current.play(); }, stopAnimation: () => { setState("stopped"); // Animation stops before the state changes lottieRef.current.stop(); }, }));
El enfoque correcto:
useEffect(() => { if (state === "running" && lottieRef.current) { lottieRef.current.play(); } else if (state === "stopped" && lottieRef.current) { lottieRef.current.stop(); } }, [state]); // Triggered when the state changes useImperativeHandle( ref, () => ({ startAnimation: () => { setState("running"); }, stopAnimation: () => { setState("stopped"); }, }), [] );
El uso de useImperativeHandle está justificado en las siguientes situaciones:
Controlar el comportamiento de los componentes secundarios: por ejemplo, para proporcionar un método de enfoque o reinicio para un componente de entrada complejo.
Ocultar detalles de implementación: el componente principal solo recibe los métodos que necesita, no el objeto de referencia completo.
Antes de utilizar useImperativeHandle, plantéese estas preguntas:
Al dominar el gancho useImperativeHandle, los desarrolladores de React pueden crear componentes más eficientes y fáciles de mantener mediante la exposición selectiva de métodos. Las técnicas descritas por el equipo de Social Discovery Group pueden ayudar a los desarrolladores a mejorar su flexibilidad, optimizar las API de sus componentes y mejorar el rendimiento general de la aplicación.
Escrito por Sergey Levkovich, ingeniero de software sénior en Social Discovery Group