00:00 / 00:00

Fecha publicación: 13 abr 2021

React ForwardRef

Cuando usamos el hook useRef nos permite de una manera fácil poder mantener la referencia a un elemento del DOM (por ejemplo un input o un select), y poder cambiar propiedades del mismo como por ejemplo asignarle el foco a ese elemento o cualquier otra.

Esto está genial si la referencia la tenemos dentro del mismo componente, pero ¿y si quisieramos pasar esa referencia desde el componente padre? Las ForwardRef nos pueden ser de gran ayuda.

ForwardRef

En este ejemplo vamos a crearnos un componente que vamos a llamar InputComponent en el que vamos a envolver un elemento (HTML) input.

Tenemos un requerimiento: cuando el usuario pulsa en un botón que se encuentra fuera del componente queremos darle el foco al elemento input interno de nuestro InputComponent.

Aquí tenemos un desafío, podemos tener un ref al input dentro de nuestro InputComponent, pero... nosotros queremos manejar esa referencia desde el componente padre, ¿qué podemos hacer? ForwardRef al rescate.

Veamos como solucionarlo mediante un ejemplo, en este caso usaremos un codesandbox con TypeScript para verlo más claro.

Primero vamos a definir nuestro componente InputComponent

import React from "react";

interface InputProps {
  label: string;
  value: string;
  onChange: (newValue: string) => void;
}

const InputComponent: React.FC<InputProps> = (props) => {
  const { label, value, onChange } = props;

  const handleChange = (event: any) => {
    onChange(event.target.value);
  };

  return <input placeholder={label} value={value} onChange={handleChange} />;
};

Vamos a añadir nuestro componente principal que instanciará dos inputComponents y un botón:

export default function App() {
  const [firstName, setFirstName] = React.useState("");
  const [secondName, setSecondName] = React.useState("");

  return (
    <div className="App">
      <InputComponent
        label="First name"
        value={firstName}
        onChange={setFirstName}
      />
      <InputComponent
        label="Second name"
        value={secondName}
        onChange={setSecondName}
      />
      <button>Set focus to second name</button>
    </div>
  );
}

Ahora viene la parte interesante: queremos que cuando el usuario pulse en el botón, el foco se asigne al segundo InputComponent.

¿Cómo podríamos intentar implementar esto con lo que sabemos de React? Podríamos exponer una propiedad ref, esto no va a funcionar, veamos por qué...

Primero en el InputComponent exponemos la propiedad ref. Es el nombre de la propiedad que tienen todos los componentes nativos en React para obtener su instancia:

import React from "react";

interface InputProps {
  label: string;
  value: string;
  onChange: (newValue: string) => void;
+ ref?: React.RefObject<HTMLInputElement>;
}

const InputComponent : React.FC<InputProps> = (props) => {
- const { label, value, onChange } = props;
+ const { label, value, onChange, ref } = props;

  const handleChange = (event: any) => {
    onChange(event.target.value);
  };

  return (
    <input
+     ref={ref}
      placeholder={label}
      value={value}
      onChange={handleChange}
    />
  );
};

Segundo lo consumimos en el componente padre:

  • Por un lado definimos una variable que tendrá la referencia.
  • Por otro cuando instanciamos nuestro segundo InputComponent le pasamos esa ref por propiedad.
export default function App() {
+ const secondInputRef = React.useRef<HTMLInputElement>(null);
  const [firstName, setFirstName] = React.useState("");
  const [secondName, setSecondName] = React.useState("");

  return (
    <div className="App">
      <InputComponent
        label="Firt name"
        value={firstName}
        onChange={setFirstName}
      />
      <InputComponent
+       ref={secondInputRef}
        label="Second name"
        value={secondName}
        onChange={setSecondName}
      />
      <button>Set focus to second name</button>
    </div>
  );
}

Ya sólo nos queda manejar esa referencia cuando el usuario pulse en el botón.

export default function App() {
  const secondInputRef = React.useRef<HTMLInputElement>(null);
  const [firstName, setFirstName] = React.useState("");
  const [secondName, setSecondName] = React.useState("");

+ const handleSetFocus = () => {
+   if (secondInputRef.current) {
+     secondInputRef.current.focus();
+   }
+ };

  return (
    <div className="App">
      <InputComponent
        label="Firt name"
        value={firstName}
        onChange={setFirstName}
      />
      <InputComponent
        ref={secondInputRef}
        label="Second name"
        value={secondName}
        onChange={setSecondName}
      />
-     <button>Set focus to second name</button>
+     <button onClick={handleSetFocus}>Set focus to second name</button>
    </div>
  );
}

Como vemos, ésto no funciona, debido a que la propiedad ref es una propiedad nativa de los componentes de React, al igual que por ejemplo la propiedad key.

Podemos hacer un workaround un poco sucio y por ejemplo cambiarle el nombre a la propiedad.

import React from "react";

interface InputProps {
  label: string;
  value: string;
  onChange: (newValue: string) => void;
- ref?: React.RefObject<HTMLInputElement>;
+ innerRef?: React.RefObject<HTMLInputElement>;
}

const InputComponent : React.FC<InputProps> = (props) => {
- const { label, value, onChange, ref } = props;
+ const { label, value, onChange, innerRef } = props;

  const handleChange = (event: any) => {
    onChange(event.target.value);
  };

  return (
    <input
-     ref={ref}
+     ref={innerRef}
      placeholder={label}
      value={value}
      onChange={handleChange}
    />
  );
};
  
export default function App() {
  const secondInputRef = React.useRef<HTMLInputElement>(null);
  const [firstName, setFirstName] = React.useState("");
  const [secondName, setSecondName] = React.useState("");

  return (
    <div className="App">
      <InputComponent
        label="Firt name"
        value={firstName}
        onChange={setFirstName}
      />
      <InputComponent
-       ref={secondInputRef}
+       innerRef={secondInputRef}
        label="Second name"
        value={secondName}
        onChange={setSecondName}
      />
      <button>Set focus to second name</button>
    </div>
  );
}

Si te fijas ahora cuando pulsas en el botón, el foco de la ventana se va al segundo InputComponent.

¿Qué ocurre si necesitamos usar la propiedad ref sin tener que inventarnos nuevas propiedades? Un ejemplo práctico, podría ser crear nuestra propia librería de componentes y dejar la posibilidad de que accedan a la referencia de cada componente.

Es hora de usar las ForwardRef.

Primero, en el InputComponent exponemos la forwardRef, si te fijas es una función que envuelve a tu componente funcional de React.

interface InputProps {
  label: string;
  value: string;
  onChange: (newValue: string) => void;
- innerRef?: React.RefObject<HTMLInputElement>;
}

- const InputComponent : React.FC<InputProps> = (props) => {
+ const InputComponent = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
-     const { label, value, onChange, innerRef } = props;
+     const { label, value, onChange } = props;

      const handleChange = (event: any) => {
        onChange(event.target.value);
      };

      return (
        <input
-         ref={innerRef}
+         ref={ref}
          placeholder={label}
          value={value}
          onChange={handleChange}
        />
      );
-   };
+   }
+ );

Segundo, volvemos a actualizar el padre:

export default function App() {
  const secondInputRef = React.useRef<HTMLInputElement>(null);
  const [firstName, setFirstName] = React.useState("");
  const [secondName, setSecondName] = React.useState("");

  return (
    <div className="App">
      <InputComponent
        label="Firt name"
        value={firstName}
        onChange={setFirstName}
      />
      <InputComponent
-       innerRef={secondInputRef}
+       ref={secondInputRef}
        label="Second name"
        value={secondName}
        onChange={setSecondName}
      />
      <button>Set focus to second name</button>
    </div>
  );
}

Este ejemplo tal cual te puede parecer un caso un poco raro pero... ponte en el escenario en el que estás validando un formulario y el equipo de usabilidad te ha pedido poner el foco en el primer campo que tenga un error... ahora empieza todo a tener más sentido ¿Verdad?

¿Te apuntas a nuestro máster?

Si te ha gustado este ejemplo y tienes ganas de aprender Front End guiado por un grupo de profesionales ¿Por qué no te apuntas a nuestro Máster Front End Online Lemoncode? Tenemos tanto edición de convocatoria con clases en vivo, como edición continua con mentorización, para que puedas ir a tu ritmo y aprender mucho.