paint-brush
Domine React diseñando API efectivas con el gancho useImperativeHandlepor@socialdiscoverygroup
Nueva Historia

Domine React diseñando API efectivas con el gancho useImperativeHandle

por Social Discovery Group11m2024/12/23
Read on Terminal Reader

Demasiado Largo; Para Leer

El gancho useImperativeHandle en React permite a los desarrolladores personalizar los métodos y las propiedades expuestas por un componente, lo que mejora la flexibilidad y la facilidad de mantenimiento. Funciona con forwardRef para proporcionar una interfaz programática para los componentes secundarios, lo que permite un control directo sobre su comportamiento. Las prácticas recomendadas incluyen aislar la lógica secundaria, simplificar la integración con bibliotecas de terceros y evitar errores comunes como matrices de dependencias incorrectas. Al usar este gancho de manera eficaz, los desarrolladores pueden crear componentes más eficientes y mejorar el rendimiento general de la aplicación.
featured image - Domine React diseñando API efectivas con el gancho useImperativeHandle
Social Discovery Group HackerNoon profile picture
0-item
1-item

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]);
  • ref — la referencia pasada al componente.
  • createHandle: una función que devuelve un objeto que será accesible a través de la referencia.
  • deps — la matriz de dependencia.


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.

Ejemplo básico


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.


Ventajas de usar ImperativeHandle en comparación con el simple uso de ref


  1. Aísla la lógica del componente secundario: permite proporcionar a los componentes principales solo los métodos necesarios.
  2. Simplifica la integración: facilita la integración de componentes React con bibliotecas que requieren acceso directo al DOM, como Lottie o Three.js.

Escenarios avanzados

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.


Errores y trampas comunes

1. Matriz de dependencias llenada incorrectamente

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:

https://use-imperative-handle.levkovich.dev/deps-is-not-correct/correct

 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:

https://use-imperative-handle.levkovich.dev/deps-empty/correct

 useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct


  1. Modificar la referencia dentro de useImperativeHandle

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:

https://use-imperative-handle.levkovich.dev/ref-modification/wrong


 useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });

El enfoque correcto:

https://use-imperative-handle.levkovich.dev/ref-modification/correct


 useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));


  1. Utilizar métodos antes de que se inicialicen


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:


https://use-imperative-handle.levkovich.dev/before-init/wrong

 const increment = useCallback(() => { childRef.current.increment(); }, [])


El enfoque correcto:

https://use-imperative-handle.levkovich.dev/before-init/correct


 const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])


  1. Problemas de sincronización entre animaciones y estados.

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:

https://use-imperative-handle.levkovich.dev/state-animation-sync/wrong


 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:


https://use-imperative-handle.levkovich.dev/state-animation-sync/correct

 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"); }, }), [] );

Conclusión

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:

  • ¿Se puede resolver la tarea de forma declarativa utilizando el estado, las propiedades o el contexto? Si es así, ese es el enfoque preferido.
  • En caso contrario y el componente necesita proporcionar una interfaz externa, utilice useImperativeHandle.


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