Históricamente las referencias en React se utilizaban exclusivamente para guardar referencias (de ahí su nombre) a instancias de componentes de clase o a nodos del DOM. Sin embargo, tenían ciertas limitaciones a la hora de trabajar con componentes funcionales. Estos componentes no se instancian, por lo que:
- No se pueden obtener referencias a los mismos
- No tienen dónde guardar sus propias referencias
Con el paso del tiempo, (1) la comunidad de React ha favorecido cada vez más el uso de componentes funcionales. Pero esto no siempre ha sido así. Al principio, estos componentes se utilizaban exclusivamente como componentes presentacionales, sin ninguna lógica de negocio (véase la dicotomía smart y dumb components).
La introducción de los hooks en React 16.8 marcó un antes y un después en el uso de los componentes funcionales y en el desarrollo de aplicaciones de React en general.
Por otra parte, (2) el concepto de referencia también ha evolucionado enormemente. Ya no se utilizan exclusivamente para acceder a elementos del DOM, sino que permiten almacenar cualquier valor que queramos mantener al margen del ciclo de vida del componente. (En la nueva documentación de React se puede ver un ejemplo muy ilustrativo con el uso de intervalos).
Las maneras en que se gestionan las referencias han cambiado drásticamente a lo largo de los años, y han aparecido nuevas APIs como [.rr-code]createRef[.rr-code] y su contrapartida para componentes funcionales [.rr-code]useRef[.rr-code], que soluciona los problemas mencionados anteriormente. En este artículo vamos a recorrer el uso de referencias en React desde sus orígenes hasta la actualidad para entender los problemas que han motivado esta evolución.
Tipos de referencias*
- Strings (wait, what?!)
- Funciones (callback refs)
- Objetos planos con propiedad [.rr-code]current[.rr-code]
* en orden de aparición y acogida en la comunidad
String refs
Básicamente, se proporciona un atributo de tipo string [.rr-code]ref="foo"[.rr-code] al nodo del que queremos almacenar la referencia. Posteriormente estas referencias serán accesibles a través del objeto [.rr-code]this.refs[.rr-code], utilizando el mismo valor como clave:
- Método legacy, totalmente desaconsejado
- No se pueden usar dentro de [.rr-code]<React.StrictMode>[.rr-code]
- No se pueden componer, por lo que no puedes obtener dos referencias a un mismo nodo
- Dependen del valor de [.rr-code]this[.rr-code], por lo que pueden dar lugar a resultados inesperados. Sobre esto, Dan Abramov tiene un ejemplo ilustrativo en un comentario de GitHub
Callback refs
Se proporciona una función que será llamada con el elemento del DOM y cada vez que este cambie:
Se debe tener en cuenta que si pasamos la función inline, esta se llamará dos veces en cada actualización, una vez con [.rr-code]null[.rr-code] y otra con el elemento. Esto sucede porque la función se crea de nuevo en cada render y, por tanto, se interpreta como una nueva referencia. Se puede evitar la doble ejecución creando la función a nivel de clase o con [.rr-code]useCallback[.rr-code], aunque generalmente no es un problema.
- Método aconsejado antes de React 16.3.
- Son funciones y, por tanto, componibles.
- Resuelven los problemas con [.rr-code]this[.rr-code]. Aunque si necesitas almacenarlas, ¿dónde lo puedes hacer?
- Solo sirven para componentes de clase (kinda). En componentes funcionales no puedes almacenar la referencia, aunque si solo necesitas acceder al elemento cada vez que cambie, podrías usarlas.
Objetos con propiedad current
Este es el método preferido y el que da vida a las APIs de [.rr-code]createRef[.rr-code] y [.rr-code]useRef[.rr-code]. Consiste en pasar un objeto como referencia, de manera que el nodo se almacene en la propiedad [.rr-code]current[.rr-code] de dicho objeto:
- La referencia al objeto (arriba [.rr-code]inputRef[.rr-code]) debe ser siempre la misma, pero su propiedad [.rr-code]current[.rr-code] es mutable.
- Mutar la propiedad [.rr-code]current[.rr-code] no causa un re-render del componente.
- Permiten almacenar referencias a componentes o nodos del DOM, pero también a cualquier otro valor.
Gestión (creación) de referencias
Al utilizar string refs, la gestión la hace enteramente React, que se encarga de almacenarlas en la propiedad [.rr-code]this.refs[.rr-code] de la instancia del componente. Al utilizar callback refs, la gestión la asume el desarrollador, y será él quien decida cómo y dónde almacenarlas cuando sea necesario.
Al utilizar objetos también podríamos hacerlo nosotros mismos, aunque hay que tener en cuenta ciertas cosas:
React nos ofrece la API [.rr-code]createRef[.rr-code] para crear objetos de referencia, que hace básicamente lo mismo que nosotros en el último ejemplo. Para utilizarla, bastaría con cambiar la creación explícita del objeto por una llamada a esta función:
React también expone la API [.rr-code]useRef[.rr-code], que es simplemente un wrapper en forma de hook sobre la anterior y que permite crear y almacenar este tipo de referencias en componentes funcionales:
Los hooks existen precisamente para darle a los componentes funcionales las posibilidades que los componentes de clase tenían de base (por ejemplo, manejo de estado local).
Cabe decir que [.rr-code]useRef[.rr-code] podría reemplazarse por [.rr-code]useState[.rr-code] sin ningún problema. React expone APIs separadas simplemente por lo extendido que está el uso de las referencias y por el valor semántico que aporta, pero el siguiente fragmento de código es equivalente al del ejemplo anterior:
Referencias a componentes funcionales
Con lo visto hasta ahora tenemos solucionado el problema de almacenar referencias en componentes funcionales, pero ¿qué pasa con las referencias a componentes funcionales?
Los componentes de clase se instancian (es decir, se crea una nueva instancia del componente cada vez que se utiliza en una aplicación), y si les damos una propiedad [.rr-code]ref[.rr-code] podremos almacenar la referencia a la instancia de dicho componente:
Sin embargo, al tratarse de simples funciones y no de clases, los componentes funcionales no se instancian. Tampoco tienen miembros ni métodos de clase, de modo que no tiene sentido pensar en utilizar este tipo de referencias con componentes funcionales. De hecho, React nos muestra un error en la consola si intentamos obtener una referencia a un componente funcional:
Referencias a nodos/elementos de componentes funcionales
Ahora bien, si no tiene sentido hablar de referencias a instancias de componentes funcionales, ¿para qué podríamos querer referenciar a un componente funcional? En muchas ocasiones, la respuesta a esta pregunta es para obtener una referencia al elemento correspondiente en el DOM (o por lo menos a uno de ellos).
Aquí entra en juego otra API de React que recibe el nombre de [.rr-code]forwardRef[.rr-code] y que permite obtener referencias a los elementos que pinta un componente funcional:
Esto resulta especialmente útil cuando los componentes tienen una correspondencia uno a uno con elementos en el DOM, ya sean sencillos (como un botón) o complejos (como una card).
Cabe decir que esto ya se podía conseguir simplemente usando una propiedad distinta de [.rr-code]ref[.rr-code] desde el componente padre, para evitar que React simplemente la ignore y nos muestre el error de antes:
La API de [.rr-code]forwardRef[.rr-code] simplemente nos ofrece una manera nativa y estándar de hacerlo, sin tener que inventarnos nuevos nombres para pasar nuestras referencias y sin tener que distinguir si el componente es funcional o no a la hora de obtener una referencia al mismo. Es decir, nos permite utilizar la propiedad [.rr-code]ref[.rr-code] en todos los casos, obteniendo un concepto de referencia más o menos consistente durante el desarrollo.
Definiendo APIs imperativas para nuestros componentes
Con esto se soluciona el problema de no poder obtener referencias a componentes funcionales... de alguna manera 🤷♂️. Tener una referencia a la instancia de un componente de clase nos permitía algo muy distinto que acceder al elemento DOM correspondiente: nos permitía acceder a la API del propio componente.
Aquí es donde entra en juego la última API de React que vamos a ver en este artículo, y que viene en forma del hook [.rr-code]useImperativeHandle[.rr-code]. Este nos permite definir APIs imperativas para nuestros componentes funcionales, y debe ser utilizado siempre con [.rr-code]forwardRef:[.rr-code]
Con esto no solo recuperamos la capacidad de exponer APIs imperativas para nuestros componentes, sino que además tenemos más control sobre las mismas al poder elegir exactamente qué métodos se exponen en estas APIs. Esto no es posible en los componentes de clase, y las referencias de instancia nos permiten acceder a métodos que tal vez no nos interese exponer, como por ejemplo los métodos del ciclo de vida como [.rr-code]componentDidMount[.rr-code] o [.rr-code]forceUpdate[.rr-code].
Referencias y recursos
- Los problemas de las string refs, comentario de Dan Abramov en GitHub.
- Refs and the DOM en la documentación (antigua) de React.
- Forwarding Refs en la documentación (antigua) de React.
- Manipulating the DOM with Refs en la documentación de React.
- Referencing Values with Refs en la documentación de React.
- [.rr-code]createRef[.rr-code] en la documentación de React.
- [.rr-code]useRef[.rr-code] en la documentación de React.
- [.rr-code]forwardRef[.rr-code] en la documentación de React.
- [.rr-code]useImperativeHandle[.rr-code] en la documentación de React.
Nota final
Desde luego las referencias son una herramienta importantísima a la hora de desarrollar aplicaciones en React. Aunque el concepto de base es sencillo, entender sus vicisitudes y aprender a utilizarlas correctamente o a sacarles el máximo partido no es moco de pavo. Y en este artículo no hemos hecho más que empezar…
¿Qué nos dices? ¿Utilizas referencias muy a menudo en tus proyectos? ¿Has utilizado alguna vez el hook [.rr-code]useImperativeHandle[.rr-code]? ¿Te gustaría saber más sobre cómo utilizamos las referencias en RedRadix o explorar algunos de sus casos de uso más complejos?
Con algunos temas como la composición de referencias, estrategias para gestionar referencias múltiples, o entender el potencial de las mismas más allá de acceder al DOM seguro que podríamos escribir otro artículo más (o incluso varios).
Si es un tema que te interesa, ¡escríbenos a comunicacion@redradix.com o en nuestros perfiles de redes sociales y lo tendremos en cuenta para futuras publicaciones.
La imagen de apertura es una fotografía de Lautaro Andreani en Unsplash.