Tratar con accesorios y estados obsoletos en los componentes funcionales de React | Programar Plus

Hay un aspecto de JavaScript que siempre me tira de los pelos: cierres. Trabajo mucho con React, y la superposición es que a veces pueden ser la causa de accesorios y estado obsoletos. Explicaremos exactamente lo que eso significa, pero el problema es que los datos que usamos para crear nuestra interfaz de usuario pueden estar totalmente equivocados de maneras inesperadas, lo cual es, ya sabes, malo.

Props y estados obsoletos

Para resumir: es cuando el código que se ejecuta de forma asincrónica tiene una referencia a una propiedad o estado que ya no está actualizado y, por lo tanto, el valor que devuelve no es el más reciente.

Para ser aún más claros, juguemos con el mismo ejemplo de referencia obsoleto que React tiene en su documentación.

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

(Demo en vivo)

Nada lujoso aquí. Tenemos un componente funcional llamado Counter. Realiza un seguimiento de cuántas veces el usuario ha hecho clic en un botón y muestra una alerta que muestra cuántas veces se hizo clic en ese botón al hacer clic en otro botón. Prueba esto:

  1. Haz clic en el botón “Haz clic en mí”. Verá que el contador de clics sube.
  2. Ahora haga clic en el botón “Mostrar alerta”. Deben pasar tres segundos y luego activar una alerta que le indica cuántas veces hizo clic en el botón “Haga clic en mí”.
  3. Ahora, haga clic en el botón “Mostrar alerta” nuevamente y haga clic rápidamente en el botón “Hacer clic en mí” antes de que active la alerta en tres segundos.

¿Mira qué pasa? El recuento que se muestra en la página y el recuento que se muestra en la alerta no coinciden. Sin embargo, el número en la alerta no es solo un número aleatorio. Ese número es el valor de la count variable tenía en el momento la función asíncrona dentro de la setTimeout se definió, que es el momento en que se hace clic en el botón “Mostrar alerta”.

Así es como funcionan los cierres. No vamos a entrar en detalles específicos de ellos en esta publicación, pero aquí hay algunos documentos que los cubren con mayor detalle.

Centrémonos en cómo podemos evitar estas referencias obsoletas con nuestros estados y accesorios.

React ofrece un consejo sobre cómo lidiar con fechas obsoletas y accesorios en la misma documentación donde se extrajo el ejemplo.

Si desea leer intencionalmente el estado más reciente de alguna devolución de llamada asíncrona, puede mantenerlo en un ref, mutarlo y leer de él.

Al mantener el valor de forma asíncrona en un ref, podemos pasar por alto las referencias obsoletas. Si necesitas saber más sobre ref en componentes funcionales, la documentación de React tiene mucha más información.

Entonces, eso plantea la pregunta: ¿Cómo podemos mantener nuestros accesorios o estado en un ref?

Hagámoslo de la manera sucia primero.

La forma sucia de almacenar accesorios y estado en una referencia

Podemos crear fácilmente una referencia usando useRef() y use count como su valor inicial. Luego, dondequiera que se actualice el estado, establecemos el ref.current propiedad al nuevo valor. Por último, utiliza ref.current en vez de count en la parte asíncrona de nuestro código.

function Counter() {
  const [count, setCount] = useState(0);
  const ref = useRef(count); // Make a ref and give it the count

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + ref.current); // Use ref instead of count
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
          ref.current = count + 1; // Update ref whenever the count changes
        }}
      >
        Click me
      </button>
      <button
        onClick={() => {
          handleAlertClick();
        }}
      >
        Show alert
      </button>
    </div>
  );
}

(Demo en vivo)

Adelante, haz lo mismo que la última vez. Haga clic en “Mostrar alerta” y luego haga clic en “Hacer clic en mí” antes de que se active la alerta en tres segundos.

¡Ahora tenemos el último valor!

He aquí por qué funciona. Cuando la función de devolución de llamada asincrónica se define dentro setTimeout, guarda una referencia a las variables que utiliza, que es count en este caso. De esta manera, cuando el estado se actualiza, React no solo cambia el valor, sino que la referencia de la variable en la memoria también es completamente diferente.

Esto significa que, incluso si el valor del estado no es primitivo, la variable con la que está trabajando en su devolución de llamada asíncrona no es la misma en la memoria. Un objeto que normalmente mantendría su referencia a lo largo de diferentes funciones ahora tiene un valor diferente.

¿Cómo usar un ref ¿resuelve esto? Si echamos un vistazo rápido a los documentos de React nuevamente, encontramos un poco de información interesante, pero fácil de perder:

[…] useRef te dare lo mismo ref objeto en cada render.

No importa lo que hagamos. A lo largo de la vida útil de su componente, React nos dará exactamente el mismo objeto ref en la memoria. Cualquier devolución de llamada, sin importar cuándo se defina o ejecute, funciona con el mismo objeto. No más referencia obsoleta.

La forma más limpia de almacenar accesorios y estado en una referencia

Seamos honestos… usando un ref así es una solución fea. ¿Qué pasa si tu estado se está actualizando en mil lugares diferentes? Ahora tienes que cambiar tu código y actualizar manualmente el ref en todos esos lugares. Eso es un no-no.

Vamos a hacer esto más escalable dando ref el valor del estado automáticamente cuando el estado cambia.

Empecemos por deshacernos del cambio manual al ref en el botón “Haz clic en mí”.

A continuación, hacemos una función llamada updateState que se llama cada vez que necesitamos cambiar el estado. Esta función toma el nuevo estado como argumento y establece el ref.current propiedad al nuevo estado y actualiza el estado también con ese mismo valor.

Finalmente, sustituyamos el original. setCount función React nos da con la nueva updateState función donde se actualiza el estado.

function Counter() {
  const [count, setCount] = useState(0);
  const ref = useRef(count);

  // Keeps the state and ref equal
  function updateState(newState) {
    ref.current = newState;
    setCount(newState);
  }

  function handleAlertClick() { ... }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          // Use the created function instead of the manual update
          updateState(count + 1);
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

(Demo en vivo)

Usar un gancho personalizado

La solución más limpia funciona bien. Hace el trabajo como la solución sucia, pero solo llama a una sola función para actualizar el estado y ref.

¿Pero adivina que? Podemos hacerlo mejor. ¿Qué sucede si necesitamos agregar más estados? ¿Y si queremos hacer esto también en otros componentes? Tomemos el estado, ref y updateState funcionar y hacerlos realmente portátiles. ¡Ganchos personalizados al rescate!

Afuera de Counter componente, vamos a definir una nueva función. vamos a nombrarlo useAsyncReference. (En realidad, se puede nombrar cualquier cosa, pero tenga en cuenta que es una práctica común nombrar ganchos personalizados con “uso” como prefijo). Nuestro nuevo gancho tendrá un solo parámetro por ahora. lo llamaremos value.

Nuestra solución anterior tenía la misma información almacenada dos veces: una en el estado y otra en el ref. Vamos a optimizar eso manteniendo el valor justo en ref esta vez. En otras palabras, crearemos un ref y dale la value parámetro como su valor inicial.

Justo después de la ref, haremos un updateState función que toma el nuevo estado y lo establece en el ref.current propiedad.

Por último, devolvemos una matriz con ref y el updateState función, muy similar a lo que hace React con useState.

function useAsyncReference(value) {
  const ref = useRef(value);

  function updateState(newState) {
    ref.current = newState;
  }

  return [ref, updateState];
}

function Counter() { ... }

¡Nos estamos olvidando de algo! Si revisamos el useRef documentación, aprendemos que actualizar un ref no desencadena una nueva renderización. Entonces, mientras ref tiene el valor actualizado, no veríamos los cambios en pantalla. Necesitamos forzar un renderizado cada vez ref se actualiza

Lo que necesitamos es un estado falso. El valor no importa. Solo va a estar allí para provocar el re-renderizado. Incluso podemos ignorar el estado y solo mantener su función de actualización. Estamos llamando a esa función de actualización forceRender y dándole un valor inicial de false.

Ahora, dentro updateState, forzamos el re-render llamando forceRender y pasándole un estado diferente al actual después de configurar ref.current a newState.

function useAsyncReference(value) {
  const ref = useRef(value);
  const [, forceRender] = useState(false);

  function updateState(newState) {
    ref.current = newState;
    forceRender(s => !s);
  }

  return [ref, updateState];
}

function Counter() { ... }

Toma el valor que tenga y devuelve lo contrario. El estado realmente no importa. Simplemente lo estamos cambiando para que React detecte un cambio en el estado y vuelva a renderizar el componente.

A continuación, podemos limpiar el Count componente y retire el utilizado previamente useState, ref y updateState luego implemente el nuevo gancho. El primer valor de la matriz devuelta es el estado en forma de ref. Seguiremos llamándolo count, donde el segundo valor es la función para actualizar el estado/ref. lo seguiremos llamando setCount.

Tambien tenemos que cambiar las referencias al conteo ya que ahora que deben estar todas count.current. Y debemos llamar setCount en lugar de llamar updateState.

function useAsyncReference(value) { ... }

function Counter() {
  const [count, setCount] = useAsyncReference(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count.current);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button
        onClick={() => {
          setCount(count.current + 1);
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

Hacer que esto funcione con accesorios

Tenemos una solución verdaderamente portátil para nuestro problema. Pero adivina qué… aún queda un poco más por hacer. Específicamente, necesitamos hacer que la solución sea compatible con props.

Tomemos el botón “Mostrar alerta” y handleAlertClick función a un nuevo componente fuera del Counter componente. vamos a llamarlo Alert y va a tomar un solo accesorio llamado count. Este nuevo componente va a mostrar la count prop value lo estamos pasando en una alerta después de un retraso de tres segundos.

function useAsyncReference(value) { ... }

function Alert({ count }) {
  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }

  return <button onClick={handleAlertClick}>Show alert</button>;
}

function Counter() { ... }

En Counter, estamos cambiando el botón “Mostrar alerta” por el Alert componente. pasaremos count.current al count apuntalar.

function useAsyncReference(value) { ... }

function Alert({ count }) { ... }

function Counter() {
  const [count, setCount] = useAsyncReference(0);

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button
        onClick={() => {
          setCount(count.current + 1);
        }}
      >
        Click me
      </button>
      <Alert count={count.current} />
    </div>
  );
}

(Demo en vivo)

Muy bien, es hora de volver a ejecutar los pasos de prueba. ¿Ver? Aunque estamos usando una referencia segura al conteo en Counter, la referencia a la count apoyo en el Alert El componente no es asincrónicamente seguro y nuestro gancho personalizado no es adecuado para usar con accesorios… todavía.

Por suerte para nosotros, la solución es bastante simple.

Todo lo que tenemos que hacer es agregar un segundo parámetro a nuestro useAsyncReference gancho llamado isProp, con false como valor inicial. Justo antes de devolver la matriz con ref y updateState, establecemos una condición. Si isProp es true, configuramos el ref.current propiedad a value y solo regreso ref.

function useAsyncReference(value, isProp = false) {
  const ref = useRef(value);
  const [, forceRender] = useState(false);

  function updateState(newState) {
    ref.current = newState;
    forceRender(s => !s);
  }

  if (isProp) {
    ref.current = value;
    return ref;
  }

  return [ref, updateState];
}

function Alert({ count }) { ... }

function Counter() { ... }

Ahora vamos a actualizar Alert por lo que se utiliza el gancho. recuerda pasar true como segundo argumento para useAsyncReference ya que estamos pasando un accesorio y no un estado.

function useAsyncReference(value) { ... }

function Alert({ count }) {
  const asyncCount = useAsyncReference(count, true);

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + asyncCount.current);
    }, 3000);
  }

  return <button onClick={handleAlertClick}>Show alert</button>;
}

function Counter() { ... }

(Demo en vivo)

Dale otra oportunidad. Ahora funciona perfectamente ya sea que uses estados o accesorios.

Una última cosa…

Hay un último cambio que me gustaría hacer. reaccionar useState Los documentos nos dicen que React se salvará de una nueva representación si el nuevo estado es idéntico al anterior. Nuestra solución no hace eso. Si volvemos a pasar el estado actual al gancho updateState función, forzaremos una nueva renderización sin importar qué. Cambiemos eso.

Pongamos el cuerpo de updateState dentro de una sentencia if y ejecutarla cuando ref.current es diferente al nuevo estado. La comparación debe hacerse con Object.is(), al igual que lo hace React.

function useAsyncReference(value, isProp = false) {
  const ref = useRef(value);
  const [, forceRender] = useState(false);

  function updateState(newState) {
    if (!Object.is(ref.current, newState)) {
      ref.current = newState;
      forceRender(s => !s);
    }
  }

  if (isProp) {
    ref.current = value;
    return ref;
  }

  return [ref, updateState];
}

function Alert({ count }) { ... }

function Counter() { ... }

¡Ahora finalmente hemos terminado!

React a veces puede parecer una caja negra llena de pequeñas peculiaridades. Esas peculiaridades pueden ser abrumadoras para tratar, como la que acabamos de abordar. Pero si eres paciente y disfrutas de los desafíos, pronto te darás cuenta de que es un marco increíble y un placer trabajar con él.