Listado

Cómo manejar referencias en React

A medida que React ha evolucionado, las referencias también han cambiado y se han convertido en una herramienta versátil y poderosa que puede ser utilizada en una variedad de situaciones. En este artículo, vamos a sumergirnos en el manejo de referencias en React para mostrarte cómo aprovechar al máximo esta funcionalidad.

Autor/a

Fecha de publicación

19/1/2024

Compartir

Twitter

LinkedIn

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*

  1. Strings (wait, what?!)
  2. Funciones (callback refs)
  3. 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:


class AutofocusInput extends React.Component {
  componentDidMount() {
    this.refs.input.focus()
  }
  render() {
    return <input ref="input" {...this.props} />
  }
}
  • 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:


class AutofocusInput extends React.Component {
  componentDidMount() {
    this.input.focus()
  }
  render() {
    return <input ref={el => (this.input = el)} {...this.props} />
  }
}

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.

// esto "funcionaría", pero cada vez que el nodo del DOM cambie volverá a 
// recibir el foco
const AutofocusInput = props => (
  <input ref={el => el.focus()} {...props} />
)

// esto también "funcionaría", pero no podrías tener más de una instancia del 
// componente al mismo tiempo (bye, bye, reusabilidad)
let inputRef

const AutofocusInput = props => {
  useEffect(() => inputRef.focus(), [])

  return <input ref={el => (inputRef = el)} {...props} />
}

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:


// cuidado, esta aproximación no permite utilizar el componente varias veces
// al mismo tiempo
const inputRef = { current: null }

const AutofocusInput = props => {
  useEffect(() => inputRef.current.focus(), [])

  return <input ref={inputRef} {...props} />
}
  • 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:


// no podemos crear los objetos inline o nunca podremos acceder a ellos
return <input ref={{ current: null }} />

// tampoco podemos hacerlo en el render del componente, o no será accesible 
// desde otros métodos del mismo
class AutofocusInput extends React.Component {
  componentDidMount() {
    // 💥 inputRef no es accesible desde aquí!
    inputRef.current.focus()
  }
  render() {
    const inputRef = { current: null }
    return <input ref={inputRef} {...this.props} />
  }
}

// tenemos que declarar estos objetos como propiedades del componente en sí y
// acceder a ellos a través de `this`
class AutofocusInput extends React.Component {
  inputRef = { current: null }
  componentDidMount() {
    // 😌 ahora sí
    this.inputRef.current.focus()
  }
  render() {
    return <input ref={this.inputRef} {...this.props} />
  }
}

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:


// utilizando la API React.createRef
class AutofocusInput extends React.Component {
  inputRef = React.createRef()
  // ...
}

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:


// utilizando React.useRef y haciendo el componente funcional
const AutofocusInput = (props) => {
  const inputRef = useRef()

  useEffect(() => {
    inputRef.current.focus()
  }, [])

  return <input ref={inputRef} {...props} />
}
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:


// utilizando React.useState podríamos conseguir el mismo resultado...
const AutofocusInput = (props) => {
  const [inputRef] = useState({ current: null })

  useEffect(() => {
    inputRef.current.focus()
  }, [])

  return <input ref={inputRef} {...props} />
}

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:


class ChildComponent extends React.Component {
  foo() { /* ... */ }
  render() { /* ... */ }
}

class ParentComponent extends React.Component {
  childRef = React.createRef()
  componentDidMount() {
    // podemos acceder a los métodos del componente hijo a través de la 
    // referencia a su instancia
    childRef.current.foo()
  }
  render() {
    // almacenamos la referencia a la instancia de un componente hijo
    return <ChildComponent ref={childRef} />
}

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:

Warning: Function components cannot be given refs. Attempts react-dom.development.js:67 to acces this ref will fail. Did you mean to use React.forwardRef()?

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:


const CustomButton = (props, ref) => <button ref={ref} {...props} />

export default forwardRef(CustomButton)

// ahora podemos obtener una referencia al elemento (DOM) desde otro componente
const SomeComponent = props => {
  const buttonRef = useRef(null)

  return <CustomButton ref={buttonRef} />
}

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:


const CustomButton = ({ buttonRef, ...props}) =>
  <button ref={buttonRef} {...props} />

// ahora podemos obtener una referencia al elemento (DOM) desde otro componente
const SomeComponent = props => {
  const buttonRef = useRef(null)

  return <CustomButton buttonRef={buttonRef} />
}

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]


const Counter = forwardRef((_, ref) => {
  const [count, setCount] = useState(0)

  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)
  const reset = () => setCount(0)

  useImperativeHandle(ref, () => ({ increment, decrement, reset }))
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  )
})

// podemos usar la API imperativa desde el componente padre
const MyComponent = () => {
  const counterRef = useRef(null)
  
  return (
    <React.Fragment>
      <Counter ref={counterRef} />
      <button onClick={() => counterRef.current.reset()}>
        Reset counter
      </button>
    </React.Fragment>
  )
}

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

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.

Relacionados

Menos es más: Cómo una infraestructura simple puede soportar un gran negocio (y una carrera)

En este artículo te mostramos cómo una pequeña inversión en DevOps e Infraestructura puede ayudarte a estar preparado para cualquier contingencia y lograr grandes beneficios. Te presentamos un caso real de éxito en el que una empresa ha logrado optimizar su entorno de trabajo, mejorar su seguridad y reducir costes.

5/4/2024

Extendiendo el principio de colocalización

Hace un tiempo publicamos un artículo sobre el principio de colocalización, una traducción libre del artículo “Colocation” de Kent C. Dodds. Hoy nos gustaría profundizar en este principio, explicando a dónde nos ha llevado en RedRadix.

26/3/2024

Button Text