React

React

Tutorial React: Hooks para optimización y actualizaciones concurrentes

React: Aprende a optimizar componentes y gestionar actualizaciones concurrentes con hooks como memo, useMemo, useCallback, useTransition y más en React.

Introducción a la memoización y conceptos de optimización

En el contexto de React, la memoización es una técnica de optimización que consiste en almacenar el resultado de funciones costosas en términos de tiempo de cómputo, para evitar cálculos redundantes. Esta técnica es especialmente útil en aplicaciones React para mejorar la eficiencia de los componentes, reduciendo el número de renders innecesarios.

React proporciona varias herramientas para implementar memoización y optimización de rendimiento, entre las que destacan memo, useMemo y useCallback. En esta sección nos centraremos en los conceptos generales de memoización y su importancia para la optimización.

memo

memo es una función de orden superior que se utiliza para memoizar componentes funcionales. Esto significa que React solo volverá a renderizar el componente si sus propiedades (props) han cambiado. Es útil para componentes que reciben las mismas props frecuentemente.

// MiComponenteMemo.jsx
import { memo } from 'react';

const MiComponenteMemo = memo(function MiComponenteMemo({ valor }) {
  console.log('Renderizando MiComponente');
  return <div>{valor}</div>;
});

export default MiComponenteMemo;

En el ejemplo anterior, MiComponenteMemo solo se renderizará si el valor cambia. De lo contrario, React reutilizará el resultado de la última renderización.

useMemo

useMemo es un hook que memoiza el resultado de una función. Se usa para evitar realizar cálculos costosos en cada renderización. useMemo solo recalcula el valor memorizado cuando una de las dependencias ha cambiado.

// ContadorElementos.jsx
import { useMemo } from 'react';

export default function ContadorElementos({ items }) {
  const itemCount = useMemo(() => {
    console.log('Calculando número de elementos');
    return items.length;
  }, [items]);

  return <div>Número de elementos: {itemCount}</div>;
}

En este ejemplo, itemCount solo se recalculará si items cambia, lo que puede ahorrar tiempo de cómputo en renderizaciones sucesivas.

useCallback

useCallback es similar a useMemo, pero se utiliza para memoizar funciones. Esto es útil cuando se pasan funciones a componentes hijos que dependen de valores que podrían cambiar en cada renderización.

// ParentComponent.jsx
import { useCallback } from 'react';

function ChildComponent({ onClick }) {
  console.log('Renderizando ChildComponent');
  return <button onClick={onClick}>Haz clic</button>;
}

export default function ParentComponent() {
  console.log('Renderizando ParentComponent');
  const handleClick = useCallback(() => {
    console.log('Botón clicado');
  }, []);

  return <ChildComponent onClick={handleClick} />;
}

En este ejemplo, handleClick se memoiza y no se recrea en cada renderización del ParentComponent, evitando renderizaciones innecesarias del ChildComponent.

Consideraciones de optimización

Es importante tener en cuenta que la memoización no siempre mejora el rendimiento y puede incluso empeorarlo si se usa en exceso o inapropiadamente. Es crucial realizar pruebas y perfiles de rendimiento para determinar si la memoización es beneficiosa en cada caso específico. Además, memo, useMemo y useCallback tienen un coste adicional de memoria, por lo que deben usarse con cuidado y solo cuando realmente se justifique.

Algunos patrones comunes que se benefician de la memoización incluyen:

  • Componentes que reciben grandes listas o datos complejos.
  • Funciones que realizan cálculos intensivos.
  • Componentes que se renderizan frecuentemente pero con las mismas propiedades.

La memoización, combinada con una arquitectura bien diseñada, puede contribuir significativamente a la eficiencia y rendimiento de las aplicaciones React.

useMemo para cálculos pesados

El hook useMemo es una herramienta crucial para optimizar componentes en React, especialmente cuando se trata de cálculos costosos que no deberían ejecutarse en cada renderización. useMemo memoiza el resultado de una función, recalculando solo cuando alguna de sus dependencias cambia. Esto es particularmente útil en situaciones donde el cálculo es intensivo en tiempo y recursos.

El uso de useMemo sigue una estructura simple:

// CostesTotales.jsx
import { useMemo } from 'react';

export default function CostesTotales({ datos }) {
  const resultadoCostoso = useMemo(() => {
    // Supongamos que esta función realiza un cálculo intensivo
    console.log('Calculando resultado costoso');
    return datos.reduce((acc, item) => acc + item.valor, 0);
  }, [datos]);

  return <div>Resultado: {resultadoCostoso}</div>;
}

En este ejemplo, resultadoCostoso se recalculará solo cuando datos cambie. Esto evita que el cálculo se ejecute en cada renderización, mejorando la eficiencia del componente.

El uso de useMemo se recomienda en los siguientes escenarios:

  • Cálculos intensivos: Cuando una función realiza operaciones complejas o intensivas que no deberían ejecutarse en cada renderización.
  • Filtrado y clasificación de grandes listas: Al manipular listas grandes, como filtrarlas o clasificarlas, useMemo puede evitar la repetición innecesaria de estas operaciones.
  • Transformaciones de datos: Cuando se realizan transformaciones costosas en datos que se pasan como propiedades a componentes hijos.

A continuación, se muestra un ejemplo más avanzado que incluye el filtrado y la clasificación de una lista de datos:

// ListaFiltrada.jsx
import { useMemo } from 'react';

export default function ListaFiltrada({ items, filtro }) {
  const elementosFiltrados = useMemo(() => {
    console.log('Filtrando elementos');
    return items.filter((item) => item.name.includes(filtro)).sort();
  }, [items, filtro]);

  return (
    <ul>
      {elementosFiltrados.map((item, index) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// App.js
import ListaFiltrada from './components/ListaFiltrada/ListaFiltrada';

export default function App() {
  const products = [
    { id: 1, name: 'Product 1', valor: 10 },
    { id: 2, name: 'Product 2', valor: 20 },
    { id: 3, name: 'Product 3', valor: 30 },
    { id: 4, name: 'Product 4', valor: 40 },
  ]
  return (
    <ListaFiltrada items={products} filtro="2" />
  );
}

En este ejemplo, elementosFiltrados solo se recalcula cuando items o filtro cambian, optimizando así el rendimiento del componente.

Es importante tener en cuenta que useMemo debe usarse con cuidado. Memoizar cálculos que no son realmente costosos puede llevar a un uso innecesario de memoria y a una complejidad adicional en el código. Además, useMemo no debería ser utilizado para memoizar objetos o arrays que se pasan como propiedades a otros componentes, ya que esto puede llevar a errores difíciles de depurar debido a la referencia de igualdad.

useCallback para memoización de funciones

El hook useCallback en React se utiliza para memoizar funciones, asegurando que no se creen nuevas instancias de estas funciones en cada renderización del componente. Esto es particularmente útil cuando se pasan funciones a componentes hijos que dependen de valores que podrían cambiar en cada renderización, evitando así renderizaciones innecesarias.

La sintaxis básica para useCallback es la siguiente:

// MiComponente.jsx
import { useCallback } from 'react';

export default function MiComponente() {
  const handleClick = useCallback(() => {
    console.log('Botón clicado');
  }, []);

  return <button onClick={handleClick}>Haz clic</button>;
}

En este ejemplo, la función handleClick se memoiza y no se recrea en cada renderización del componente MiComponente. Esto es especialmente útil cuando handleClick se pasa como una propiedad a un componente hijo.

Un caso común de uso de useCallback es cuando se tiene un componente padre que pasa una función a un componente hijo. Sin useCallback, la función se recrearía en cada renderización del componente padre, lo que podría causar renderizaciones innecesarias del componente hijo.

// ChildComponent.jsx
export default function ChildComponent({ onClick }) {
  console.log('Renderizando ChildComponent');
  return <button onClick={onClick}>Haz clic</button>;
}

// ParentComponent.jsx
import { useCallback } from 'react';
import ChildComponent from '../ChildComponent/ChildComponent';

export default function ParentComponent() {
  const handleClick = useCallback(() => {
    console.log('Botón clicado');
  }, []);

  return <ChildComponent onClick={handleClick} />;
}

En este ejemplo, handleClick se memoiza en el componente ParentComponent y se pasa como propiedad a ChildComponent. Esto evita que ChildComponent se renderice innecesariamente cada vez que ParentComponent se renderiza, siempre y cuando handleClick no cambie.

Es importante tener en cuenta que useCallback acepta una lista de dependencias como segundo argumento. La función solo se volverá a crear si alguna de las dependencias cambia. Esto es crucial para asegurar que la función memoizada se comporte correctamente.

// MiComponente.jsx
import { useState, useCallback } from 'react';

export default function MiComponente() {
  const [contador, setContador] = useState(0);

  const incrementarContador = useCallback(() => {
    setContador(prevContador => prevContador + 1);
  }, []);

  return (
    <>
      <p>Contador: {contador}</p>
      <button onClick={incrementarContador}>Incrementar contador</button>
    </>
  )
}

En este ejemplo, incrementarContador se memoiza y solo se vuelve a crear si alguna de las dependencias (en este caso, ninguna) cambia. Esto puede ser útil en casos donde la función depende de valores específicos del estado o de las propiedades.

Una consideración importante al usar useCallback es importante evitar su uso excesivo. Memoizar funciones que no causan renderizaciones innecesarias puede llevar a un código más complejo y difícil de mantener sin ganancias significativas en rendimiento. Es crucial realizar perfiles de rendimiento para determinar si el uso de useCallback es beneficioso en cada caso específico.

useTransition y useDeferredValue para actualizaciones concurrentes

En React, useTransition y useDeferredValue son hooks que permiten manejar actualizaciones concurrentes, mejorando la experiencia del usuario al mantener la interfaz responsiva durante operaciones costosas.

useTransition permite definir actualizaciones de estado como transiciones, lo que ayuda a evitar bloqueos en la interfaz de usuario durante actualizaciones intensivas. Se utiliza para dividir actualizaciones urgentes y no urgentes, priorizando la respuesta de la interfaz.

// MiComponente.jsx
import { useState, useTransition } from 'react';

export default function MiComponente() {
  const [isPending, startTransition] = useTransition();
  const [input, setInput] = useState('');
  const [list, setList] = useState([]);

  const handleChange = (e) => {
    setInput(e.target.value);
    startTransition(() => {
      const newList = Array.from({ length: 20000 }, (_, i) => `${e.target.value} ${i}`);
      setList(newList);
    });
  };

  return (
    <div>
      <input type="text" value={input} onChange={handleChange} />
      {isPending ? <p>Cargando...</p> : <ul>{list.map((item, index) => <li key={index}>{item}</li>)}</ul>}
    </div>
  );
}

En este ejemplo, handleChange utiliza startTransition para actualizar la lista de forma concurrente. Mientras la lista se actualiza, la interfaz muestra un mensaje de carga, manteniéndose reactiva.

useDeferredValue permite diferir la actualización de un valor hasta que las actualizaciones más urgentes se completen, útil para valores derivados de cálculos costosos.

// MiComponente.jsx
import { useState, useDeferredValue, useMemo } from 'react';

export default function MiComponente() {
  const [input, setInput] = useState('');
  const deferredInput = useDeferredValue(input);
  const list = useMemo(() => {
    return Array.from({ length: 20000 }, (_, i) => `${deferredInput} ${i}`);
  }, [deferredInput]);

  return (
    <div>
      <input type="text" value={input} onChange={(e) => setInput(e.target.value)} />
      <ul>{list.map((item, index) => <li key={index}>{item}</li>)}</ul>
    </div>
  );
}

En este ejemplo, deferredInput difiere la actualización de input en la lista hasta que las actualizaciones más urgentes se completen, mejorando el rendimiento de la interfaz.

Consideraciones:

  • useTransition y useDeferredValue son útiles para mantener la interfaz de usuario responsiva durante actualizaciones costosas.
  • useTransition permite dividir actualizaciones en urgentes y no urgentes, mientras que useDeferredValue difiere las actualizaciones de valores derivados.
  • Ambos hooks deben ser usados con cuidado para evitar complejidad innecesaria en el código y asegurar una experiencia de usuario óptima.

useId para generación de identificadores únicos

useId es un hook introducido en React 18 que se utiliza para generar identificadores únicos y estables que pueden ser utilizados en elementos HTML. Esto es especialmente útil en situaciones donde se necesita un identificador único para asociar etiquetas <label> con elementos de formulario, manejar accesibilidad, o cualquier otro caso que requiera identificadores únicos en el DOM.

La sintaxis básica para useId es la siguiente:

import React, { useId } from 'react';

export default function MiComponente() {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>Nombre:</label>
      <input id={id} type="text" name="nombre" />
    </div>
  );
}

En este ejemplo, useId genera un identificador único que se utiliza tanto en el atributo htmlFor de la etiqueta <label> como en el atributo id del elemento <input>. Esto asegura que el <label> esté correctamente asociado con el <input>.

Una característica importante de useId es que los identificadores generados son estables a través de renderizaciones y montajes del componente. Esto significa que el identificador no cambiará entre renderizaciones, lo cual es crucial para mantener la consistencia en aplicaciones que dependen de identificadores únicos.

useId también se puede utilizar en componentes que se renderizan dinámicamente. Por ejemplo, en una lista de elementos de formulario generados dinámicamente:

// ListaFormularios.jsx
import { useId } from 'react';

export default function ListaFormularios({ items }) {
  return (
    <div>
      {items.map((item, index) => {
        const id = useId();
        return (
          <div key={index}>
            <label htmlFor={id}>{item.label}</label>
            <input id={id} type="text" name={item.name} />
          </div>
        );
      })}
    </div>
  );
}

En este caso, cada iteración del map genera un nuevo identificador único para cada par de <label> y <input>, asegurando que no haya conflictos de identificadores en el DOM.

Es importante tener en cuenta que useId no debe ser utilizado para generar identificadores que necesiten ser persistentes entre sesiones de usuario o que deban ser compartidos a través de diferentes instancias de la aplicación. Para estos casos, es más apropiado utilizar identificadores generados en el backend o mediante alguna otra lógica de generación de identificadores persistentes.

Otra consideración es que useId no debe ser utilizado dentro de loops o condiciones que cambien entre renderizaciones, ya que esto podría llevar a inconsistencias en los identificadores generados. En su lugar, se recomienda utilizarlo en la parte superior del componente para asegurar la estabilidad del identificador.

// FormularioCondicional.jsx
import { useId } from 'react';

export default function FormularioCondicional({ mostrarInput }) {
  const id = useId(); // Generar el ID fuera de la condición

  return (
    <div>
      {mostrarInput && (
        <>
          <label htmlFor={id}>Condicional:</label>
          <input id={id} type="text" name="condicional" />
        </>
      )}
    </div>
  );
}

// App.js
import { useState } from 'react';
import FormularioCondicional from './components/FormularioCondicional/FormularioCondicional';

export default function App() {
  const [checked, setChecked] = useState(false);

  return (
    <>
      <label>
        Checkbox:
        <input type="checkbox" checked={checked} onChange={() => setChecked(!checked)} />
      </label>
      <FormularioCondicional mostrarInput={checked} />
    </>
  );
}

En este ejemplo, el identificador id se genera una vez, independientemente de si mostrarInput es verdadero o falso, asegurando que el identificador sea estable.

useId es un hook útil para la generación de identificadores únicos y estables en React, asegurando la consistencia y accesibilidad en componentes que requieren identificadores únicos en el DOM. Su uso adecuado puede mejorar la calidad del código y la experiencia del usuario en aplicaciones React.

useOptimistic para UI optimista

Advertencia El Hook useOptimistic está actualmente disponible solo en React Canary y canales experimentales, lo que significa que aún está en fase de prueba y podría cambiar antes del lanzamiento final. Si decides usarlo, asegúrate de estar al día con las actualizaciones de React y de utilizar la versión RC más reciente. Para habilitarlo, añade las versiones @rc de react y react-dom con:

npm add react@rc react-dom@rc

El hook useOptimistic en React es una herramienta valiosa para gestionar estados optimistas en la interfaz de usuario. El estado optimista permite que la interfaz reaccione inmediatamente a las acciones del usuario, asumiendo que la operación será exitosa, mientras la confirmación de la operación se procesa en segundo plano. Esto mejora la experiencia del usuario al proporcionar una respuesta inmediata y fluida.

Un caso práctico sería usar useOptimistic en un componente de lista de tareas donde se desea añadir una tarea y actualizar la interfaz de usuario de inmediato:

// ListaTareas.jsx
import { useOptimistic, useState, useRef, useTransition } from "react";

const initialState = [
  { id: 1, titulo: "Tarea 1" },
]

export default function ListaTareas() {
  const formRef = useRef(null);
  const [tasks, setTasks] = useState(initialState);
  const [isPending, startTransition] = useTransition();
  const [optimisticTasks, setOptimisticTasks] = useOptimistic(tasks, (state, newTask) => [
    ...state,
    newTask
  ]);

  async function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    const titulo = formData.get("task");
    if (titulo.length === 0) return; // No se puede agregar una tarea vacía
    let newTask = { id: Math.random().toString(36).slice(2), titulo, isPending: true };
    setOptimisticTasks(newTask);
    formRef.current.reset();
    //Intenta agregar la tarea a la lista de tareas, si la petición es exitosa, la añade al estado real de tareas
    try {
      newTask = await addTodo(newTask);
      setTasks((prevTodos) => [...prevTodos, newTask]);
    } catch (error) {
      console.error(error);
    }
  }

  //  Simulamos una petición HTTP
  async function addTodo({ titulo }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // Simulamos un error aleatorio
        const error = Math.random() < 0.2; // 20% de probabilidad de error

        if (error) {
          reject(new Error('Error simulado'));
        } else {
          // Simulamos que la petición es exitosa
          const data = { id: Math.random().toString(36).slice(2), titulo };
          resolve(data);
        }
      }, 2000); // 2 segundos
    });
  }

  return (
    <>
      <form onSubmit={(e) => startTransition(() => handleSubmit(e))} ref={formRef}>
        <label>
          Nombre de la tarea:
          <input type="text" name="task" placeholder="Nueva tarea" />
        </label>
        <button>Añadir tarea</button>
      </form>
      <ul>
        {optimisticTasks.map((tarea) => (
          <li key={tarea.id}>{tarea.titulo} {tarea.isPending && <small>adding...</small>} </li>
        ))}
      </ul>
    </>
  );
}

En este ejemplo, el componente ListaTareas permite a los usuarios añadir tareas a una lista de manera optimista, utilizando varios hook de React como useOptimistic, useState, useRef, y useTransition.

El componente inicializa una lista de tareas (tasks) utilizando useState, con un estado inicial que contiene una única tarea. Se utiliza useOptimistic para manejar el estado optimista de la lista de tareas. Esto permite que la interfaz se actualice inmediatamente cuando el usuario añade una nueva tarea, incluso antes de que la operación sea confirmada por el servidor.

El formulario de entrada se gestiona mediante una referencia (formRef) y, al enviarse, se desencadena la función handleSubmit. handleSubmit utiliza useTransition para iniciar la transición, lo que permite que la actualización de la interfaz sea más fluida y no bloquee el hilo principal.

Al enviar el formulario, se crea una nueva tarea con un estado optimista (isPending: true), que se añade inmediatamente a la lista de tareas visibles gracias a setOptimisticTasks. Mientras la tarea aparece en la lista, se simula una petición HTTP con la función addTodo. Si la petición es exitosa, la tarea se confirma y se añade definitivamente al estado real (tasks). Si la petición falla, se revierte la operación y se maneja el error en el bloque catch, donde se podría notificar al usuario.

La función addTodo simula una petición a un servidor con un retraso de 2 segundos. Hay un 20% de probabilidad de que la petición falle, lo que ayuda a ilustrar cómo se maneja el flujo optimista bajo condiciones de red inestables o errores de servidor.

La lista de tareas se renderiza utilizando el estado optimista. Si una tarea está pendiente de confirmación (isPending: true), se muestra un indicador "adding..." junto a la tarea.

Consideraciones importantes al usar useOptimistic:

  • Manejo de Errores: Aunque este ejemplo maneja los errores con un bloque catch mostrando por consola un mensaje de error, es fundamental extender esta lógica para notificar al usuario de la falla.
  • Optimización del Rendimiento: useTransition se utiliza para hacer que la experiencia del usuario sea más fluida, especialmente en situaciones donde las operaciones optimistas podrían introducir una carga adicional en la interfaz de usuario.
Certifícate en React con CertiDevs PLUS

Ejercicios de esta lección Hooks para optimización y actualizaciones concurrentes

Evalúa tus conocimientos de esta lección Hooks para optimización y actualizaciones concurrentes con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Todas las lecciones de React

Accede a todas las lecciones de React y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A React Y Su Ecosistema

React

Introducción Y Entorno

Instalar React Y Crear Nuevo Proyecto

React

Introducción Y Entorno

Introducción A Jsx

React

Componentes

Introducción A Componentes

React

Componentes

Componentes Funcionales

React

Componentes

Eventos En React

React

Componentes

Props Y Manejo De Datos Entre Componentes

React

Componentes

Renderizado Condicional

React

Componentes

Renderizado Iterativo Con Bucles

React

Componentes

Manejo De Clases Y Estilos

React

Componentes

Introducción A Los Hooks

React

Hooks

Estado Y Ciclo De Vida De Los Componentes

React

Hooks

Hooks Para Manejo De Estado Y Efectos Secundarios

React

Hooks

Hooks Para Gestión De Estado Complejo Y Contexto

React

Hooks

Hooks Para Optimización Y Actualizaciones Concurrentes

React

Hooks

Introducción A React Router

React

Navegación Y Enrutamiento

Definición Y Manejo De Rutas

React

Navegación Y Enrutamiento

Rutas Anidadas Y Rutas Dinámicas

React

Navegación Y Enrutamiento

Navegación Programática Y Redireccionamiento

React

Navegación Y Enrutamiento

Nuevos Métodos Create De React Router

React

Navegación Y Enrutamiento

Solicitudes Http Con Fetch Api

React

Interacción Http Con Backend

Solicitudes Http Con Axios

React

Interacción Http Con Backend

Estado Local Con Usestate Y Usereducer

React

Servicios Y Gestión De Estado

Estado Global Con Context Api

React

Servicios Y Gestión De Estado

Estado Global Con Redux Toolkit

React

Servicios Y Gestión De Estado

Custom Hooks Para Servicios Compartidos

React

Servicios Y Gestión De Estado

Certificados de superación de React

Supera todos los ejercicios de programación del curso de React y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender los conceptos básicos de memoización en React.
  • Implementar la optimización del rendimiento utilizando memo, useMemo y useCallback.
  • Gestionar actualizaciones concurrentes con useTransition y useDeferredValue.
  • Generar identificadores únicos y estables con useId.
  • Aplicar técnicas de UI optimista con useOptimistic.