Artículo original escrito por: Reed Barger
Artículo original: 5 Key React Lessons the Tutorials Don't Teach You
Traducido y adaptado por: Juan C. Guaña

Hay muchos conceptos y lecciones esenciales que los desarrolladores de React necesitan saber y que simplemente no se tratan en la mayoría de los tutoriales.

He seleccionado cuidadosamente los temas que creo que son algunos de los más importantes que debes conocer, pero pocos artículos se han dedicado a cubrirlos en detalle.

Echemos un vistazo a cinco lecciones clave de React que vale la pena conocer y que quizás no encuentres en otro lugar.

1. Cómo se actualiza realmente el estado de React

Como desarrollador de React, sabes que el estado se puede crear y actualizar con los hooks useState y useReducer.

Pero, ¿qué sucede exactamente cuando actualiza el estado de un componente con cualquiera de estos hooks? ¿El estado se actualiza inmediatamente o se hace en algún momento posterior?

Veamos el siguiente código, que es una aplicación de contador muy simple. Como era de esperar, puede hacer clic en el botón y nuestro contador aumenta en 1.

import React from 'react';

export default function App() {
  const [count, setCount] = React.useState(0)

  function addOne() {
    setCount(count + 1);
  }

  return (
    <div>
      <h1>Count: {count}</h1> {/* 1 (as we expect) */}

      <button onClick={addOne}>+ 1</button>
    </div>
  );
}

Pero, ¿qué pasa si intentamos agregar una línea adicional, que también actualiza nuestro recuento en uno? ¿Qué crees que sucederá?

Cuando haces clic en el botón, ¿nuestro contador mostrado aumentará en uno o dos?

import React from 'react';

export default function App() {
  const [count, setCount] = React.useState(0)

  function addOne() {
    setCount(count + 1);
    setCount(count + 1);
  }

  return (
    <div>
      <h1>Count: {count}</h1> {/* 1?! */}

      <button onClick={addOne}>+ 1</button>
    </div>
  );
}

¡Si ejecutamos este código, vemos que se incrementa solo en uno! A pesar de intentar incrementar el contador en uno dos veces, con dos actualizaciones de estado separadas.

¿Por qué nuestro contador muestra 1, a pesar de incrementar claramente el estado en 1 dos veces?

La razón de esto es que React programa una actualización de estado que se realizará cuando actualizamos el estado por primera vez. Debido a que solo está programado y no se realiza de inmediato (es asíncrono y no síncrono), nuestra variable contador no se actualiza antes de intentar actualizarla por segunda vez.

En otras palabras, debido a que la actualización del estado está programada, no se realiza de inmediato, la segunda vez que llamamos a setCount, count sigue siendo solo 0, no 1.

La forma en que podemos solucionar esto para actualizar el estado de manera confiable, a pesar de que las actualizaciones de estado son asincrónicas, es usar la función interna que está disponible dentro de la función setter de useState.

Esto nos permite obtener el estado anterior y devolver el valor que queremos que tenga en el cuerpo de la función interna. Cuando usamos este patrón, vemos que se incrementa en dos como queríamos originalmente:

import React from 'react';

export default function App() {
  const [count, setCount] = React.useState(0)

  function addOne() {
    setCount(prevCount => prevCount + 1); // 1
    setCount(prevCount => prevCount + 1); // 2
  }

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={addOne}>+ 1</button>
    </div>
  );
}

2. Es mejor usar varios efectos en lugar de uno

Al realizar un efecto secundario, la mayoría de los desarrolladores de React usarán useEffect solo una vez e intentarán realizar múltiples efectos secundarios dentro de la misma función de efecto.

¿Cómo se ve eso? A continuación, puedes ver dónde obtenemos datos de publicaciones y comentarios en un hook useEffect para colocarlos en sus respectivas variables de estado:

import React from "react";

export default function App() {
  const [posts, setPosts] = React.useState([]);
  const [comments, setComments] = React.useState([]);

  React.useEffect(() => {
    // fetching post data
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((res) => res.json())
      .then((data) => setPosts(data));

    // fetching comments data
    fetch("https://jsonplaceholder.typicode.com/comments")
      .then((res) => res.json())
      .then((data) => setComments(data));
  }, []);

  return (
    <div>
      <PostsList posts={posts} />
      <CommentsList comments={comments} />
    </div>
  );
}

En lugar de intentar agrupar todos sus efectos secundarios en un solo hook de efecto, del mismo modo que puedes usar el hook useState más de una vez, puede usar varias veces useEffect.

Hacerlo nos permite separar nuestras diferentes acciones en diferentes efectos para una mejor separación de preocupaciones.

Una mejor separación de preocupaciones es un beneficio importante que brindan los hooks de React en comparación con el uso de métodos de ciclo de vida dentro de los componentes de la clase.

En métodos como componentDidMount, por ejemplo, era necesario incluir cualquier acción que queramos realizar después de que se montara nuestro componente. No puede dividir sus efectos secundarios en varios métodos – cada método del ciclo de vida en las clases se puede usar una vez y solo una vez.

El principal beneficio de React hooks es que podemos dividir nuestro código en función de lo que está haciendo. No solo podemos separar en múltiples efectos las acciones que estamos realizando después de renderizar, sino que también podemos co-ubicar nuestro estado:

import React from "react";

export default function App() {
  const [posts, setPosts] = React.useState([]);
  React.useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((res) => res.json())
      .then((data) => setPosts(data));
  }, []);

  const [comments, setComments] = React.useState([]);
  React.useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/comments")
      .then((res) => res.json())
      .then((data) => setComments(data));
  }, []);

  return (
    <div>
      <PostsList posts={posts} />
      <CommentsList comments={comments} />
    </div>
  );
}

Esto significa que podemos poner el hook useState con el hook useEffect con el que está relacionado. Esto ayuda a organizar nuestro código mucho mejor y a comprender mejor lo que está haciendo de un vistazo.

3. No optimizar las funciones que actualizan el estado (useState, useReducer)

Una tarea común cada vez que pasamos una función callback desde llamada de un componente padre a un componente hijo es evitar que se vuelva a crear, a menos que sus argumentos hayan cambiado.

Podemos realizar esta optimización con la ayuda del hook useCallback.

useCallback fue creado específicamente para funciones callback que se pasan a componentes hijos para asegurarse de que no se vuelvan a recrear innecesariamente, lo que genera un impacto en el rendimiento de nuestros componentes cada vez que se vuelve a renderizar.

Esto se debe a que cada vez que nuestro componente padre se vuelve a renderizar, también se volverán a renderizar todos los componentes hijos. Esto es lo que hace que nuestras funciones callback se vuelvan a crear en cada re-renderización.

Sin embargo, si estamos usando una función setter para actualizar el estado que hemos creado con los hooks useState o useReducer, no necesitamos envolver eso con useCallback.

En otras palabras, no es necesario hacer esto:

import React from "react";

export default function App() {
  const [text, setText] = React.useState("")

  // Don't wrap setText in useCallback (it won't change as is)
  const handleSetText = React.useCallback((event) => {
    setText(event.target.value);
  }, [])

  return (
    <form>
      <Input text={text} handleSetText={handleSetText} />
      <button type="submit">Submit</button>
    </form>
  );
}

function Input({ text, handleSetText }) {
  return(
    <input type="text" value={text} onChange={handleSetText}  />
  )
}

La razón proviene directamente de la documentación de React:

React garantiza que la identidad de la función setState es estable y no cambiará en subsecuentes renderizados. Es por eso que es seguro omitirla de la lista de dependencias de useEffect o useCallback.

Por lo tanto, no solo no necesitamos optimizarlo innecesariamente con useCallback, sino que tampoco necesitamos incluirlo como una dependencia dentro de useEffect porque no cambiará.

Es importante tener en cuenta esto porque, en muchos casos, puede reducir el código que necesitamos usar. Y lo más importante, es un intento improductivo de optimizar el código, ya que puede incurrir en problemas de rendimiento propios.

4. El hook useRef puede preservar el estado en todas las renderizaciones

Como desarrolladores de React, a veces es muy útil poder hacer referencia a un elemento de React dado con la ayuda de un ref (referencia). Creamos refs en React con la ayuda del hook useRef.

Sin embargo, es importante tener en cuenta que useRef no solo es útil para hacer referencia a un determinado elemento DOM. La documentación de React lo dice en sí misma:

El objeto “ref” es un contenedor genérico cuya propiedad current es mutable y puede contener cualquier valor.

Existen ciertos beneficios para poder almacenar y actualizar valores con useRef. Nos permite almacenar un valor que no estará en la memoria y que no se borrará al volver a renderizar.

Si quisiéramos realizar un seguimiento de un valor en las representaciones con la ayuda de una variable simple, se reinicializaría cada vez que se procese el componente. Sin embargo, si usas una referencia, el valor almacenado en ella permanecerá constante en todas las representaciones del componente.

¿Cuál es un caso de uso para aprovechar useRef de esta manera?

Esto podría ser útil en el caso de que quisiéramos realizar un efecto secundario determinado solo en el renderizado inicial, por ejemplo:

import React from "react";

export default function App() {
  const [count, setCount] = React.useState(0);
  const ref = React.useRef({ hasRendered: false });

  React.useEffect(() => {
    if (!ref.current.hasRendered) {
      ref.current.hasRendered = true;
      console.log("perform action only once!");
    }
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
}

Intente ejecutar este código usted mismo.

Como verá, no importa cuántas veces se haga clic en el botón, se actualice el estado y se vuelva a renderizar, la acción que queremos realizar (ver console.log) solo se realiza una vez.

5. Cómo evitar que la aplicación en React se bloquee

Una de las lecciones más importantes que los desarrolladores de React deben saber, especialmente si no han enviado una aplicación React a la web, es qué hacer con los errores no detectados.

En el siguiente ejemplo, intentamos mostrar un componente Header en nuestra aplicación, pero estamos realizando una acción que da como resultado un error. Es decir, intentar obtener una propiedad a partir de un valor nulo:

import React from "react";

export default function App() {
  return (
    <>
      <Header />
    </>
  );
}

function Header() {
  const user = null;

  return <h1>Hello {user.name}</h1>; // error!
}

Si enviamos este código a producción, veremos una pantalla en blanco exactamente como esta:

5-key-lessons-1

¿Por qué no vemos nada?

Nuevamente, podemos encontrar la respuesta para esto dentro de la documentación de React:

A partir de React 16, los errores que no fueron detectados por ningún límite de error resultarán en el desmontaje de todo el árbol de componentes de React.

Mientras está en desarrollo, verás un gran mensaje de error rojo con un seguimiento de la pila que le indica dónde se puede encontrar el error. Sin embargo, cuando tu aplicación esté activa, solo verás una pantalla en blanco.

Este no es el comportamiento deseado que quieres para tu aplicación.

Pero hay una manera de solucionarlo, o al menos mostrarles a tus usuarios algo que les diga que se produjo un error si la aplicación falla accidentalmente. Puedes envolver el árbol de componentes en lo que se llama un límite de error (error boundary).

Los límites de error son componentes que nos permiten detectar errores y mostrar a los usuarios un mensaje de respaldo que les dice que ocurrió algo incorrecto. Eso podría incluir instrucciones sobre cómo descartar el error (como volver a cargar la página).

Podemos usar un límite de error con la ayuda del paquete react-error-boundary. Podemos ajustarlo al componente que creemos que es propenso a errores. También se puede envolver alrededor de todo el árbol de componentes de la aplicación:

import React from "react";
import { ErrorBoundary } from "react-error-boundary";

export default function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Header />
    </ErrorBoundary>
  );
}

function Header() {
  const user = null;

  return <h1>Hello {user.name}</h1>;
}

function ErrorFallback({ error }) {
  return (
    <div role="alert">
      <p>Oops, there was an error:</p>
      <p style={{ color: "red" }}>{error.message}</p>
    </div>
  );
}

También puedes mostrar el mensaje de error como desees y diseñarlo como lo haría con cualquier componente normal.

El resultado que obtenemos cuando ocurre un error es mucho mejor:

5-key-lessons-2