Quizá no necesites un Efecto

Los Efectos son una puerta de escape del paradigma de React. Te permiten «salir» de React y sincronizar tus componentes con algún sistema externo como un widget externo a React, una red o el DOM del navegador. Si no hay algún sistema externo involucrado (por ejemplo, si quieres actualizar el estado de un componente cuando algunas props o estados cambien), no deberías necesitar un Efecto. Eliminar los Efectos innecesarios hará que tu código sea más fácil de comprender, más rápido de ejecutar y menos propenso a errores.

Aprenderás

  • Por qué y cómo eliminar los Efectos innecesarios de tus componentes
  • Cómo almacenar en caché cálculos costosos sin Efectos
  • Cómo restablecer y ajustar el estado de los componentes sin Efectos
  • Cómo compartir la lógica entre manejadores de eventos
  • Qué lógica debe ser trasladada a los manejadores de eventos
  • Cómo notificar a los componentes padre sobre cambios

Cómo eliminar Efectos innecesarios

Hay dos casos comunes en los que no necesitarás Efectos:

  • No es necesario usar Efectos a la hora de transformar datos para su renderizado. Por ejemplo, supongamos que quieres filtrar una lista antes de mostrarla. Podrías sentirte tentado a escribir un Efecto que actualice una variable de estado cuando la lista cambie. Sin embargo, esto no es eficiente. Ya que cuando actualices el estado de tu componente, React primero llamará a las funciones de tu componente para determinar lo que debe aparecer en la pantalla. Luego React «confirmará» estos cambios en el DOM, actualizando la pantalla. Luego de ello React ejecutará tus Efectos. Si tu Efecto también actualiza inmediatamente el estado, ¡esto reiniciará todo el proceso desde cero! Para evitar renderizaciones innecesarias, transforma todos los datos en el nivel superior de tus componentes. Ese código se volverá a ejecutar automáticamente cada vez que tus props o tu estado cambien.
  • No necesitarás Efectos para manejar eventos de usuario. Por ejemplo, digamos que quieres enviar una petición POST a /api/buy y mostrar una notificación cuando el usuario compra un producto. En el manejador de evento de clic del botón de compra, se sabe exactamente lo que ha pasado. En el momento en que se ejecuta un Efecto, no sabes qué hizo el usuario (por ejemplo, qué botón se pulsó). Es por esto que generalmente se manejan los eventos de usuario en sus respectivos manejadores de eventos.

Pero necesitarás Efectos para sincronizar tus componentes con sistemas externos. Por ejemplo, puedes escribir un Efecto que mantenga a un widget de jQuery sincronizado con el estado de React. También puedes cargar datos con Efectos: por ejemplo, puedes sincronizar los resultados de búsqueda con la consulta de búsqueda actual. Ten en cuenta que los frameworks modernos proporcionan mecanismos de carga de datos incorporados mucho más eficientes que escribir Efectos directamente en tus componentes.

Para ayudarte a obtener una buena intuición, ¡veamos algunos ejemplos concretos comunes!

Actualizar un estado en relación a otras props o estados

Supongamos que tienes un componente con dos variables de estado: firstName para un nombre y lastName para un apellido. Y que quieres calcular un nombre completo como fullName a partir de la concatenación de los anteriores. Además, te gustaría que fullName se actualizara cada vez que firstName o lastName cambien. Tu primer impulso podría ser añadir una variable de estado fullName y actualizarla en un Efecto:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');

// 🔴 Desaconsejado: estado redundante y Efecto innecesario
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}

Esto es más complicado de lo necesario. También es ineficiente: desencadena un ciclo completo de renderizado con un valor antiguo para fullName, y luego inmediatamente vuelve a renderizar con el valor actualizado. Es mejor que elimines tanto la variable de estado como el efecto:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Buena práctica: calcularlo durante el renderizado
const fullName = firstName + ' ' + lastName;
// ...
}

Cuando algo puede ser calculado a partir de las props o el estado existente, no lo pongas en un nuevo estado. Es mejor calcularlo durante el renderizado. Esta manera hace que tu código sea más rápido (evitas las actualizaciones extra «en cascada»), más simple (te ahorras algo de código), y menos propenso a errores (evitas errores causados por diferentes variables de estado que no están sincronizadas entre sí). Si este enfoque es nuevo para ti, pensar en React tiene algunas orientaciones sobre lo que debería ir en el estado.

Almacenar en caché los cálculos costosos

El objetivo del siguiente componente TodoList es mostrar una lista de tareas pendientes, en él tenemos el cálculo de las tareas pendientes a mostrar (visibleTodos) tomando las tareas (todos) que recibe por props, filtrándolas de acuerdo al filtro (filter) que también recibe por props. Podrías sentirte tentado a almacenar el resultado de este cálculo en una variable de estado y actualizarlo en un efecto:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');

// 🔴 Desaconsejado: : estado redundante y Efecto innecesario
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

// ...
}

Como en el ejemplo anterior, esto es innecesario e ineficiente. En primera instancia, elimina el estado y el Efecto:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Esto está bien si getFilteredTodos() no es demasiado lento.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}

Por lo general, ¡este código está bien! Pero quizás getFilteredTodos() es lento o tienes muchos todos. En ese caso, no querrás volver a calcular getFilteredTodos() si alguna variable de estado no relacionada como newTodo ha cambiado.

Puedes almacenar en caché (o «memoizar») un cálculo costoso envolviéndolo en un Hook de React useMemo:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ No se volverá a ejecutar a menos que todos o filter cambien
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}

O, escrito en una sola línea:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ No se volverá a ejecutar getFilteredTodos() a menos que todos o filter cambien
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}

Esto le dice a React que no quieres que la función interna se vuelva a ejecutar a menos que todos o filter hayan cambiado. React recordará el valor de retorno de getFilteredTodos() durante la renderización inicial. Durante los siguientes renderizados, verificará si todos o filter son diferentes. Si son los mismos que la última vez, useMemo devolverá el último resultado que ha almacenado. Pero si son diferentes, React llamará a la función interna nuevamente (y almacenará su resultado).

La función que envuelves en el Hook de React useMemo se ejecuta durante el renderizado, por lo que solo funcionará para cálculos puros.

Profundizar

¿Cómo saber si un cálculo es costoso?

En general, a menos que estés creando o iterando sobre miles de objetos, probablemente no sea costoso. Si quieres tener más confianza, puedes imprimirlo en la consola y medir el tiempo que pasa en ese fragmento de código:

console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');

Realiza la interacción que estás midiendo (por ejemplo, escribir en la entrada de texto). Luego verás registros como filter array: 0.15ms en tu consola. Si el tiempo total registrado suma una cantidad significativa (digamos, 1ms o más), podría tener sentido memoizar ese cálculo. Como experimento, puedes envolver el cálculo en useMemo para verificar si el tiempo total registrado ha disminuido para esa interacción o no:

console.time('filter array');
const visibleTodos = useMemo(() => {
return getFilteredTodos(todos, filter); // Se omite si todos y filter no han cambiado
}, [todos, filter]);
console.timeEnd('filter array');

useMemo no hará que el primer renderizado sea más rápido. Solo te ayuda a evitar trabajo innecesario en las actualizaciones.

Ten en cuenta que tu máquina probablemente sea más rápida que la de tus usuarios, por lo que es una buena idea probar el rendimiento con una ralentización artificial. Por ejemplo, Chrome ofrece una opción de limitación de CPU para esto.

También ten en cuenta que medir el rendimiento en desarrollo no te dará los resultados más precisos. (Por ejemplo, cuando el »Modo Estricto» está activado, verás que cada componente se renderiza dos veces en lugar de una.) Para obtener los tiempos más precisos, compila tu aplicación para producción y pruébala en un dispositivo como el que tienen tus usuarios.

Reiniciar de cero el estado cuando una prop cambie

Este componente ProfilePage recibe una prop userId. La página contiene una entrada de texto de comentario, y utiliza una variable de estado comment para guardar su valor. Un día, te das cuenta de un problema: cuando navegas de un perfil a otro, el estado comment no se reinicia. Como resultado, es fácil publicar accidentalmente un comentario en el perfil de un usuario incorrecto. Para solucionar el problema, quieres limpiar la variable de estado comment cada vez que el userId cambie:

export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');

// 🔴 Desaconsejado: Reiniciar el estado según cambio de prop en un Efecto
useEffect(() => {
setComment('');
}, [userId]);
// ...
}

Esto es ineficiente porque ProfilePage y sus hijos primero se renderizarán con el valor obsoleto, y luego se renderizarán de nuevo. También es complicado porque necesitarías hacer esto en cada componente que tenga algún estado dentro de ProfilePage. Por ejemplo, si la interfaz de usuario del comentario está anidada, también querrías limpiar el estado del comentario anidado.

En lugar de ello, puedes decirle a React que cada perfil de usuario es conceptualmente un perfil diferente dándole una key explícita. Divide tu componente en dos y pasa un atributo key del componente exterior al interior:

export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}

function Profile({ userId }) {
// ✅ Esto y cualquier otro estado a continuación se reiniciará automáticamente al cambiar la key
const [comment, setComment] = useState('');
// ...
}

Usualmente, React conserva el estado cuando el mismo componente se renderiza en el mismo lugar. Al pasar userId como una key al componente Profile, le estás pidiendo a React que trate a dos componentes Profile con diferentes userId como dos componentes diferentes que no deberían compartir ningún estado. Cuando la key (que has establecido como userId) cambie, React recreará el DOM y reiniciará el estado del componente Profile y todos sus hijos. Ahora el campo comment se borrará automáticamente cuando se navegue entre perfiles.

Ten en cuenta que en este ejemplo, solo el componente externo ProfilePage es exportado y visible para otros archivos en el proyecto. Los componentes que renderizan ProfilePage no necesitan pasar la key a este: pasan userId como una prop regular. El hecho de que ProfilePage lo pase como una key al componente interno Profile es un detalle de implementación.

Actualizar parte del estado cuando cambie una prop

A veces, querrás resetear o ajustar una parte del estado según un cambio de prop, pero no todo.

Este componente List recibe una lista de items como una prop y mantiene el elemento seleccionado en la variable de estado selection. Tu objetivo es resetear selection a null siempre que la prop items reciba un array diferente:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 🔴 Desaconsejado: Ajustar el estado según un cambio de prop en un Efecto
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}

Esto tampoco es ideal. Debido a que cada vez que los items cambien, List y sus componentes hijos renderizarán con un valor selection obsoleto al comienzo. Luego React actualizará el DOM y ejecutará los Efectos. Finalmente, la llamada a setSelection(null) provocará otro renderizado de List y sus componentes hijos, nuevamente reiniciando por completo este proceso.

Comienza por eliminar el Efecto. En su lugar, ajusta el estado directamente durante el renderizado:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// Una mejor práctica: Actualizar el estado al renderizar
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}

Almacenar información de renderizados anteriores de esta manera puede ser difícil de entender, pero es mejor que actualizar el mismo estado en un Efecto. En el ejemplo anterior, setSelection se llama directamente durante un renderizado. React volverá a renderizar List inmediatamente después de su declaración return. React aún no ha renderizado los hijos de List ni ha actualizado el DOM, por lo que esto permite que los hijos de List omitan el renderizar el valor obsoleto de selection.

Cuando actualizas un componente durante el renderizado, React descarta el JSX devuelto e intenta inmediatamente el renderizado nuevamente. Para evitar reintentos en cascada muy lentos, React solo te permite actualizar el estado del mismo componente durante un renderizado. Si actualizas el estado de otro componente durante un renderizado, verás un error. Una condición como items !== prevItems es necesaria para evitar bucles. Puedes ajustar el estado de esta manera, pero cualquier otro efecto secundario (como cambiar el DOM o establecer timeouts) debe permanecer en manejadores de eventos o Efectos para mantener a los componentes puros.

Aunque este patrón es más eficiente que un Efecto, la mayoría de los componentes tampoco deberían necesitarlo. No importa cómo lo hagas, ajustar el estado basado en las props u otro estado hace que tu flujo de datos sea más difícil de entender y depurar. Siempre verifica si puedes resetear todo el estado con una key o calcular todo durante el renderizado en su lugar. Por ejemplo, en lugar de almacenar (y resetear) el ítem seleccionado, puedes almacenar el ID del ítem seleccionado:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Una mejor práctica: Calcular todo durante el renderizado
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}

Ahora no hay necesidad de «ajustar» el estado en lo absoluto. Si el elemento con el ID seleccionado está en la lista, permanecerá seleccionado. Si no lo está, la selection calculada durante el renderizado será null porque no se encontró ningún elemento coincidente. Este comportamiento es diferente, pero se podría decir que es mejor porque la mayoría de los cambios en items preservan la selección.

Compartir la lógica entre manejadores de eventos

Supongamos que tienes una página de producto con dos botones (Comprar y Pagar) los cuales te permiten comprar ese producto. Quieres mostrar una notificación cada vez que el usuario pone el producto en el carrito. Llamar a showNotification() en los manejadores de clic de ambos botones se siente repetitivo, por lo que podrías estar tentado a colocar esta lógica en un Efecto:

function ProductPage({ product, addToCart }) {
// 🔴 Evita: Lógica específica para evento dentro de un Efecto
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);

function handleBuyClick() {
addToCart(product);
}

function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}

Este Efecto es innecesario. Incluso es muy probable que cause errores. Por ejemplo, digamos que tu aplicación «recuerda» el carrito de compras entre recargas de la página. Si añades un producto al carrito una vez y refrescas la página, la notificación aparecerá de nuevo. Y seguirá apareciendo cada vez que refresques la página del producto. Esto se debe a que product.isInCart ya será true en la carga de la página, por lo que el Efecto anterior llamará a showNotification().

Cuando no estés seguro de si algún código debería estar en un Efecto o en un manejador de eventos, pregúntate por qué este código necesita ejecutarse. Usa Efectos solo para el código que debe ejecutarse porque el componente se mostró al usuario. En este ejemplo, la notificación debería aparecer porque el usuario presionó el botón, ¡no porque se mostró la página! Elimina el Efecto y coloca la lógica compartida en una función llamada desde ambos manejadores de eventos:

function ProductPage({ product, addToCart }) {
// ✅ Buena práctica: La lógica específica para eventos se llama desde los manejadores de eventos
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}

function handleBuyClick() {
buyProduct();
}

function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}

Esto tanto elimina el Efecto innecesario como corrige el error.

Realizar una petición POST

Este componente Form envía dos tipos de solicitudes POST. Envía un evento de analíticas cuando se monta. Por otra parte, cuando llenas el formulario y haces clic en el botón Enviar, enviará una solicitud POST al endpoint /api/register:

function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ Buena práctica: Esta lógica debe ejecutarse porque el componente se mostró al usuario
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

// 🔴 Evita: Lógica específica de evento dentro de un Efecto
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);

function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}

Apliquemos el mismo criterio que en el ejemplo anterior.

La solicitud POST de analíticas debe permanecer en un Efecto. Esto es porque la razón para enviar el evento de analíticas es que el formulario se mostró. (Se dispararía dos veces en desarrollo, pero mira aquí cómo lidiar con eso.)

Sin embargo, la solicitud POST a /api/register no es causada por el formulario siendo mostrado al usuario. Solo quieres enviar la solicitud en un momento específico en el tiempo: cuando el usuario presiona el botón. Solo debería ocurrir en esa interacción en particular. Elimina el segundo Efecto y mueve esa solicitud POST al manejador de eventos:

function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ Buena práctica: Esta lógica se ejecuta porque el componente se mostró al usuario
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

function handleSubmit(e) {
e.preventDefault();
// ✅ Buena práctica: La lógica para el evento se ejecuta en un manejador de eventos
post('/api/register', { firstName, lastName });
}
// ...
}

Cuando estaás por decidir si poner alguna lógica en un manejador de eventos o un Efecto, la pregunta principal que debes responder es qué tipo de lógica es desde la perspectiva del usuario. Si esta lógica es causada por una interacción en particular, mantenla en el manejador de eventos. Si es causada porque el usuario ve el componente en la pantalla, mantenla en el Efecto.

Cadenas de cálculos

A veces podrías sentirte tentado a encadenar Efectos que ajustan una pieza de estado basada en otro estado:

function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);

// 🔴 Evitar: Cadenas de Efectos que ajustan el estado solo para activarse entre sí
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);

useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);

useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);

useEffect(() => {
alert('Good game!');
}, [isGameOver]);

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}

// ...

Hay dos problemas con este código.

Un problema es que es muy ineficiente: el componente (y sus hijos) tienen que volver a renderizarse entre cada llamada a un actualizador set- en la cadena. En el ejemplo anterior, en el peor de los casos (setCard → render → setGoldCardCount → render → setRound → render → setIsGameOver → render) hay tres renderizaciones innecesarias del árbol hacia debajo.

Incluso si no fuera lento, a medida que tu código evoluciona, te encontrarás con casos en los que la «cadena» que escribiste no se ajusta a los nuevos requisitos. Imagina que estás añadiendo una forma de repasar el historial de movimientos del juego. Lo harías actualizando cada variable de estado a un valor del pasado. Sin embargo, establecer el estado card a un valor del pasado desencadenaría de nuevo la cadena de Efectos y cambiaría los datos que estás mostrando. Este tipo de código es a menudo rígido y frágil.

En este caso, es mejor calcular lo que puedas durante la renderización, y ajustar el estado en el manejador de eventos:

function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);

// ✅ Calcula lo que puedas durante la renderización
const isGameOver = round > 5;

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}

// ✅ Calcula todo el siguiente estado en el manejador de eventos
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}

// ...

Esto es mucho más eficiente. Además, si implementas una forma de ver el historial del juego, ahora podrás establecer cada variable de estado a un movimiento del pasado sin activar la cadena de Efectos que ajusta todos los demás valores. Si necesitas reutilizar la lógica entre varios manejadores de eventos, puedes extraer una función y llamarla desde esos manejadores.

Recuerda que dentro de los manejadores de eventos, el estado se comporta como una instantánea. Por ejemplo, incluso después de llamar a setRound(round + 1), la variable round reflejará el valor en el momento en que el usuario hizo clic en el botón. Si necesitas usar el siguiente valor para cálculos, defínelo manualmente como const nextRound = round + 1.

En algunos casos, no puedes calcular el siguiente estado directamente en el manejador de eventos. Por ejemplo, imagina un formulario con múltiples desplegables donde las opciones del siguiente desplegable dependen del valor seleccionado del desplegable anterior. Entonces, una cadena de Efectos es apropiada porque estás sincronizando con la red.

Inicializar la aplicación

Algunas lógicas sólo deben ejecutarse una vez cuando se carga la aplicación.

Podrías estar tentado a colocarla en un Efecto en el componente de nivel superior:

function App() {
// 🔴 Evitar: Efectos con lógica que sólo debería ejecutarse una vez
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}

Sin embargo, rápidamente descubrirás que se ejecuta dos veces en desarrollo. Esto puede causar problemas: por ejemplo, quizás invalida el token de autenticación porque la función no fue diseñada para ser llamada dos veces. En general, tus componentes deberían ser resistentes a ser desmontados. Esto incluye tu componente App de nivel superior.

Aunque puede que nunca se desmonte en la práctica en producción, seguir las mismas restricciones en todos los componentes facilita mover y reutilizar el código. Si alguna lógica debe ejecutarse una vez por carga de la aplicación en lugar de una vez por montaje del componente, añade una variable de nivel superior para rastrear si ya se ha ejecutado:

let didInit = false;

function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Solo se ejecuta una vez por carga de la aplicación
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}

También puedes ejecutarlo durante la inicialización del módulo y antes de que la aplicación se renderice:

if (typeof window !== 'undefined') { // Comprueba si estamos ejecutándolo en el navegador.
// ✅ Solo se ejecuta una vez por carga de la aplicación
checkAuthToken();
loadDataFromLocalStorage();
}

function App() {
// ...
}

El código en el nivel superior se ejecuta una vez cuando se importa tu componente, incluso si no termina siendo renderizado. Para evitar ralentizaciones o comportamientos inesperados al importar componentes arbitrarios, no abuses de este patrón. Mantén la lógica de inicialización de la aplicación en los módulos de componentes raíz como App.js o en el punto de entrada de tu aplicación.

Notificar a los componentes padre sobre cambios de estado

Digamos que estás escribiendo un componente Toggle con un estado interno isOn que puede ser true o false. Hay varias formas diferentes de cambiarlo (haciendo clic o arrastrando). Quieres notificar al componente padre cada vez que el estado interno de Toggle cambia, así que expones un evento onChange y lo llamas desde un Efecto:

function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

// 🔴 Evitar: El manejador onChange se ejecuta demasiado tarde
useEffect(() => {
onChange(isOn);
}, [isOn, onChange])

function handleClick() {
setIsOn(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
setIsOn(true);
} else {
setIsOn(false);
}
}

// ...
}

Como antes, esto no es ideal. El Toggle actualiza su estado primero, y React actualiza la pantalla. Luego React ejecuta el Efecto, que llama a la función onChange pasada desde un componente padre. Ahora el componente padre actualizará su propio estado, iniciando otro proceso de renderizado. Sería mejor hacer todo en un solo paso.

Elimina el Efecto y en su lugar actualiza el estado de ambos componentes dentro del mismo manejador de eventos:

function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

function updateToggle(nextIsOn) {
// ✅ Bien: Realiza todas las actualizaciones durante el evento que las causó
setIsOn(nextIsOn);
onChange(nextIsOn);
}

function handleClick() {
updateToggle(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}

// ...
}

Con este enfoque, tanto el componente Toggle como su componente padre actualizan su estado durante el evento. React agrupa las actualizaciones de diferentes componentes, por lo que sólo habrá un paso de renderizado.

También podrías eliminar el estado por completo, y en su lugar recibir isOn del componente padre:

// ✅ También está bien: el componente está completamente controlado por su padre
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}

// ...
}

«Elevar el estado» te permite que el componente padre controle completamente el Toggle cambiando el estado propio del padre. Esto significa que el componente padre tendrá que contener más lógica, pero habrá menos estado en general de qué preocuparse. ¡Cada vez que intentes mantener dos variables de estado diferentes sincronizadas, intenta elevar el estado en su lugar!

Pasar datos al componente padre

Este componente Child carga algunos datos y luego los pasa al componente Parent en un Efecto:

function Parent() {
const [data, setData] = useState(null);
// ...
return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
const data = useSomeAPI();
// 🔴 Evitar: Pasar datos de hijo a padre en un Efecto
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}

En React, los datos fluyen de los componentes padres a sus hijos. Cuando ves algo incorrecto en la pantalla, puedes rastrear de dónde viene la información subiendo la cadena de componentes hasta que encuentres qué componente pasa la prop incorrecta o tiene el estado incorrecto. Cuando los componentes hijos actualizan el estado de sus componentes padres en Efectos, el flujo de datos se vuelve muy difícil de rastrear. Como tanto el hijo como el padre necesitan los mismos datos, deja que el componente padre recupere esos datos, y pásalos hacia abajo al hijo en su lugar:

function Parent() {
const data = useSomeAPI();
// ...
// ✅ Buena práctica: Pasar los datos desde padres a hijos
return <Child data={data} />;
}

function Child({ data }) {
// ...
}

Esto es más sencillo y mantiene el flujo de datos predecible: los datos fluyen hacia abajo desde el padre al hijo.

Suscribirse a una fuente de datos externa

A veces, tus componentes pueden necesitar suscribirse a algunos datos fuera del estado de React. Estos datos podrían provenir de una biblioteca de terceros o de una API incorporada en el navegador. Como estos datos pueden cambiar sin que React lo sepa, necesitas suscribir manualmente tus componentes a ellos. Esto se hace a menudo con un Efecto, por ejemplo:

function useOnlineStatus() {
// No ideal: Suscripción manual en un Efecto
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}

updateState();

window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

Aquí, el componente se suscribe a una fuente de datos externa (en este caso, la API navigator.onLine del navegador). Dado que esta API no existe en el servidor (por lo que no puede usarse para el HTML inicial), inicialmente el estado se establece en true. Siempre que el valor de esa fuente de datos cambia en el navegador, el componente actualiza su estado.

Aunque es común usar Efectos para esto, React tiene un Hook diseñado específicamente para suscribirse a una fuente externa que se prefiere en su lugar. Elimina el Efecto y reemplázalo con una llamada al Hook de React useSyncExternalStore:

function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}

function useOnlineStatus() {
// ✅ Buena práctica: Suscribirse a una fuente externa con un Hook integrado
return useSyncExternalStore(
subscribe, // React no volverá a suscribirse mientras pases la misma función
() => navigator.onLine, // Acá va el cómo obtener el valor en el cliente
() => true // Acá va el cómo obtener el valor en el servidor
);
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

Este enfoque es menos propenso a errores que sincronizar manualmente datos mutables al estado de React con un Efecto. Generalmente, escribirás un Hook personalizado como useOnlineStatus() anteriormente para que no debas repetir este código en los componentes individuales. Lee más sobre cómo suscribirte a fuentes externas desde componentes de React.

Cargar datos

Muchas aplicaciones usan Efectos para iniciar la carga de datos. Es bastante común escribir un Efecto de carga de datos como este:

function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);

useEffect(() => {
// 🔴 Evita: Obtener datos sin lógica de limpieza
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

No necesitas mover esta carga de datos a un manejador de eventos.

Esto puede parecer una contradicción con los ejemplos anteriores donde necesitabas poner la lógica en los manejadores de eventos. Sin embargo, considera que no es el evento de escritura el que es la razón principal para cargar datos. Las entradas de búsqueda a menudo se pre-rellenan desde la URL, y el usuario puede navegar hacia atrás y hacia adelante sin tocar la entrada de texto.

No importa de dónde vengan page y query. Mientras este componente sea visible, querrás mantener los results sincronizados con los datos de la red para la page y query actuales. Por eso es un Efecto.

Sin embargo, el código anterior tiene un error. Imagina que escribes «hola» rápidamente. Entonces la query cambiará de «h», a «ho», «hol», y «hola». Esto iniciará búsquedas separadas, pero no hay garantía sobre el orden en que llegarán las respuestas. Por ejemplo, la respuesta «hol» puede llegar después de la respuesta «hola». Como «hol» llamará a setResults() al final, estarás mostrando los resultados de búsqueda incorrectos. Esto se llama una «condición de carrera»: dos solicitudes diferentes «compitieron» entre sí y llegaron en un orden diferente al que esperabas.

Para corregir la condición de carrera, tendrás que agregar una función de limpieza para ignorar las respuestas obsoletas:

function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

Esto asegura que cuando tu Efecto carga datos, todas las respuestas excepto la última solicitada serán ignoradas.

Manejar las condiciones de carrera no es la única dificultad al implementar la carga de datos. También podrías querer pensar en almacenar en caché las respuestas (para que el usuario pueda hacer clic en «Atrás» y ver la pantalla anterior instantáneamente), en cómo obtener datos en el servidor (para que el HTML inicial renderizado por el servidor contenga el contenido obtenido en lugar de un spinner), y en cómo evitar cascadas de red (para que un hijo pueda cargar datos sin esperar a cada padre).

Estos problemas se aplican a cualquier biblioteca de interfaz de usuario, no solo a React. Resolverlos no es trivial, por lo que los frameworks modernos proporcionan mecanismos de carga de datos integrados más eficientes que la carga de datos con Efectos.

Si no usas un framework (y no quieres construir el tuyo propio) pero te gustaría hacer que la carga de datos desde Efectos sea más ergonómica, considera extraer tu lógica de obtención a un Hook personalizado como en el siguiente ejemplo:

function SearchResults({ query }) {
const [page, setPage] = useState(1);
const params = new URLSearchParams({ query, page });
const results = useData(`/api/search?${params}`);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return data;
}

Probablemente también querrás agregar alguna lógica para el manejo de errores y para seguir si el contenido está cargando. Puedes construir un Hook como este tú mismo o utilizar una de las muchas soluciones ya disponibles en el ecosistema de React. Aunque esto por sí solo no será tan eficiente como usar el mecanismo de carga de datos integrado de un marco, trasladar la lógica de carga de datos a un Hook personalizado facilitará la adopción de una estrategia de carga de datos eficiente más tarde.

En general, cada vez que debas recurrir a escribir Efectos, busca cuándo puedes extraer una pieza de funcionalidad a un Hook personalizado con una API más declarativa y diseñada específicamente como el useData anterior. Cuantas menos llamadas innecesarias a useEffect tengas en tus componentes, más fácil será el mantenimiento de tu aplicación.

Recapitulación

  • Si puedes calcular algo durante el renderizado, no necesitas un Efecto.
  • Para almacenar en caché cálculos costosos, utiliza useMemo en lugar de useEffect.
  • Para restablecer el estado de un árbol de componentes completo, pásale una key diferente.
  • Para restablecer una porción de estado en respuesta a un cambio de prop, configúralo durante el renderizado.
  • El código que se ejecuta porque un componente se mostró al usuario debería estar en Efectos, el resto debería estar en eventos.
  • Si necesitas actualizar el estado de varios componentes, es mejor hacerlo durante un solo evento.
  • Siempre que intentes sincronizar variables de estado en diferentes componentes, considera elevar el estado.
  • Puedes obtener datos con Efectos, pero necesitas implementar la lógica de limpieza para evitar las «condiciones de carrera».

Desafío 1 de 4:
Transform data without Effects

The TodoList below displays a list of todos. When the «Show only active todos» checkbox is ticked, completed todos are not displayed in the list. Regardless of which todos are visible, the footer displays the count of todos that are not yet completed.

Simplify this component by removing all the unnecessary state and Effects.

import { useState, useEffect } from 'react';
import { initialTodos, createTodo } from './todos.js';

export default function TodoList() {
  const [todos, setTodos] = useState(initialTodos);
  const [showActive, setShowActive] = useState(false);
  const [activeTodos, setActiveTodos] = useState([]);
  const [visibleTodos, setVisibleTodos] = useState([]);
  const [footer, setFooter] = useState(null);

  useEffect(() => {
    setActiveTodos(todos.filter(todo => !todo.completed));
  }, [todos]);

  useEffect(() => {
    setVisibleTodos(showActive ? activeTodos : todos);
  }, [showActive, todos, activeTodos]);

  useEffect(() => {
    setFooter(
      <footer>
        {activeTodos.length} todos left
      </footer>
    );
  }, [activeTodos]);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={showActive}
          onChange={e => setShowActive(e.target.checked)}
        />
        Show only active todos
      </label>
      <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} />
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </li>
        ))}
      </ul>
      {footer}
    </>
  );
}

function NewTodo({ onAdd }) {
  const [text, setText] = useState('');

  function handleAddClick() {
    setText('');
    onAdd(createTodo(text));
  }

  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={handleAddClick}>
        Add
      </button>
    </>
  );
}