Cómo detectar los efectos de React que no necesitas
Usa eslint-plugin-react-you-might-not-need-an-effect para detectar efectos de React innecesarios y refactorizar el estado derivado, los eventos y los reinicios por props.
eslint-plugin-react-you-might-not-need-an-effect es un plugin para ESLint y Oxlint que señala las llamadas a useEffect que la documentación de React considera innecesarias, como los efectos que derivan estado, retransmiten eventos o envían datos hacia un componente padre.
Has leído You Might Not Need an Effect, has asentido con cada párrafo y luego has abierto una base de código con cuatrocientas llamadas a useEffect y ninguna forma práctica de saber cuáles son las problemáticas.
En este artículo se explica cómo instalar el plugin, qué reporta sobre un componente realista, cómo refactorizar los cuatro hallazgos más habituales y cuándo conviene suprimir una advertencia en lugar de corregirla. Si necesitas repasar el hook en sí, consulta esta guía sobre el hook useEffect de React.
Puntos clave
- eslint-plugin-react-you-might-not-need-an-effect incluye nueve reglas y dos presets de flat config:
recommendedreporta todas las reglas como advertencias ystrictlas reporta todas como errores. Los proyectos con el formato heredado .eslintrc disponen de los presets equivalenteslegacy-recommendedylegacy-strict. - Calcula durante el render los valores que dependen únicamente de las props y el estado actuales, en lugar de copiarlos al estado mediante un efecto.
- Para reiniciar todo el estado de un componente cuando cambia una prop, como el ID de un cliente, renderiza el componente usando esa prop como su
key. - Los efectos que mantienen React sincronizado con un sistema externo, como un temporizador, una API del DOM, un widget ajeno a React o una conexión de red, son precisamente la razón de ser de
useEffect, así que consérvalos. - Suprime una advertencia con un comentario
eslint-disable-next-lineque indique el nombre de la regla y un motivo después de--.
¿Qué hace que un efecto de React sea innecesario?
La mayoría de los efectos innecesarios adoptan una de dos formas: un efecto que recalcula un valor y lo guarda en el estado, o un efecto que observa una parte del estado y actualiza otra para que coincida. En ambos casos, React confirma (commit) un render con el valor antiguo, ejecuta el efecto y vuelve a renderizar con el valor corregido.
El render adicional provocado por un efecto innecesario es visible para los usuarios. Las grabaciones de sesión (session replays) de componentes con muchos efectos pueden mostrar una lista filtrada vacía o un total provisional durante un frame, antes de que el segundo render lo complete. La solución casi siempre consiste en eliminar el estado, no en ajustar el efecto.
Instalar y ejecutar el plugin
El plugin se instala como dependencia de desarrollo y se integra en la flat config de ESLint mediante su preset recommended. Si ya gestionas varios plugins de ESLint, convive con ellos sin conflictos.
npm install --save-dev eslint-plugin-react-you-might-not-need-an-effect
// eslint.config.js
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
import globals from "globals";
export default [
reactYouMightNotNeedAnEffect.configs.recommended,
{
languageOptions: {
globals: { ...globals.browser },
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
];
Los ejemplos utilizan la versión 1.0.2, publicada el 20 de agosto de 2026 según el CHANGELOG del plugin. Ese mismo changelog indica que no-pass-ref-to-parent y no-empty-effect se eliminaron antes de la 1.0, así que bórralas de las configuraciones antiguas. Quienes usen Oxlint cargan el plugin a través del campo jsPlugins de Oxlint. Los IDs de las reglas usan el prefijo en kebab-case react-you-might-not-need-an-effect/ en Oxlint, en las configuraciones heredadas y en la flat config cuando se usan los presets, porque estos registran el plugin con el nombre de su paquete. El prefijo en camelCase reactYouMightNotNeedAnEffect/ solo se aplica si registras tú mismo el plugin con esa clave, como hace el ejemplo de regla individual del README.
A continuación se muestra un componente con cuatro efectos, cada uno escrito para coincidir con el patrón documentado de una regla:
import { useEffect, useState } from "react";
import { postOrder, useCustomerHistory } from "./api";
export function OrderPanel({ customerId, items, onLoaded }) {
const [total, setTotal] = useState(0);
const [submitted, setSubmitted] = useState(false);
const [note, setNote] = useState("");
const history = useCustomerHistory(customerId);
// Effect 1
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
}, [items]);
// Effect 2
useEffect(() => {
if (submitted) postOrder(customerId, items, note);
}, [submitted, customerId, items, note]);
// Effect 3
useEffect(() => {
onLoaded(history);
}, [history, onLoaded]);
// Effect 4
useEffect(() => {
setNote("");
setSubmitted(false);
}, [customerId]);
return (
<form onSubmit={(e) => { e.preventDefault(); setSubmitted(true); }}>
<textarea value={note} onChange={(e) => setNote(e.target.value)} />
<p>Total: {total}</p>
<button type="submit">Place order</button>
</form>
);
}
Ejecuta npx eslint src/OrderPanel.jsx. Con recommended, los hallazgos aparecen como advertencias con el ID de la regla al final de cada línea. Cada efecto se corresponde con la regla que activa:
| Efecto | Patrón | Regla |
|---|---|---|
| 1 | Estado calculado a partir de props | no-derived-state |
| 2 | Flag de estado usado como disparador de un evento | no-event-handler |
| 3 | Datos obtenidos que se pasan a un callback del padre | no-pass-data-to-parent |
| 4 | Estado que se borra cuando cambia una prop | no-adjust-state-on-prop-change (una advertencia por cada setter) |
El efecto 4 es el caso que resuelve key. no-reset-all-state-on-prop-change reporta esta misma forma en componentes más sencillos, pero en un componente como este, que lee su estado durante el render, la versión 1.0.2 reporta cada reinicio bajo no-adjust-state-on-prop-change.
El conjunto completo de reglas, con la alternativa para cada una:
| Regla | Sustituye el efecto por |
|---|---|
no-derived-state | Un valor calculado durante el render |
no-chain-state-updates | Una única actualización de estado consolidada |
no-event-handler | Código en el manejador de eventos |
no-adjust-state-on-prop-change | Un estado reestructurado o un cálculo en tiempo de render |
no-reset-all-state-on-prop-change | La prop pasada como key |
no-pass-live-state-to-parent | Estado elevado (lifted state) o un valor devuelto por un hook |
no-pass-data-to-parent | Obtención de datos en el padre, con el resultado pasado como props |
no-external-store-subscription | useSyncExternalStore |
no-initialize-state | Un valor inicial en useState() |
El plugin rastrea cadenas de llamadas y arrays de dependencias, por lo que añade carga al proceso de linting. El perfilado de reglas de ESLint (TIMING=1) muestra cuánto cuesta en tu propio repositorio. El README también recomienda activar react-hooks/exhaustive-deps, porque el plugin da por hecho que tus arrays de dependencias son correctos, y typescript-eslint/no-floating-promises, que le ayuda a reconocer las llamadas a funciones asíncronas.
Estado derivado: calcúlalo durante el render
Cuando un valor se puede obtener a partir de las props y el estado que ya tienes, calcúlalo durante el render. No guardes una copia en el estado para sincronizarla con un efecto. La documentación de React lo trata en actualizar el estado en función de las props o del estado.
// Before
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
}, [items]);
// After
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
Ahora el total es correcto desde el primer render. Si el cálculo resulta costoso, envuélvelo en useMemo. No lo devuelvas al estado.
Lógica de manejadores de eventos: muévela al manejador
Un efecto que espera a que se active un flag de estado en un manejador de envío o de clic es, en realidad, un manejador de eventos disfrazado. Ejecuta esa lógica en el propio manejador. El razonamiento se explica en Separating Events from Effects.
// Before
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) postOrder(customerId, items, note);
}, [submitted, customerId, items, note]);
// After
function handleSubmit(e) {
e.preventDefault();
postOrder(customerId, items, note);
}
La versión “antes” tiene un bug real: editar la nota después de enviar vuelve a publicar el pedido. Desde la 0.10.1, no-event-handler también señala las props usadas de esta manera y, desde la 0.10.2, reporta en el punto de uso en lugar de en la declaración.
Obtención de datos y envío de datos hacia arriba
Un componente hijo que obtiene datos y se los pasa a su padre mediante un efecto debería delegar esa tarea en el padre, en la carga de datos del framework o en una librería de data fetching, y recibir el resultado como props. React describe esta misma inversión en pasar datos al padre.
// Before: child fetches, then pushes up
const history = useCustomerHistory(customerId);
useEffect(() => {
onLoaded(history);
}, [history, onLoaded]);
// After: parent owns the data
function OrderPage({ customerId, items }) {
const history = useCustomerHistory(customerId); // loader, library, or parent effect
return <OrderPanel customerId={customerId} items={items} history={history} />;
}
Cuando los datos fluyen en un único sentido, queda claro qué componente es su propietario, y OrderPanel ya no necesita la prop de callback onLoaded solo para informar de lo que ha cargado.
Reiniciar el estado cuando cambia una prop: usa key
Para reiniciar todo el estado de un componente cuando cambia una prop, como el ID de un cliente, renderiza el componente usando esa prop como su key, de modo que React monte una instancia nueva. El patrón está documentado en reiniciar todo el estado cuando cambia una prop.
// Before: Effect 4 inside OrderPanel
useEffect(() => {
setNote("");
setSubmitted(false);
}, [customerId]);
// After: delete the effect; in the parent
<OrderPanel key={customerId} customerId={customerId} items={items} history={history} />
no-adjust-state-on-prop-change está pensada para la versión parcial de este caso, en la que solo una parte del estado cambia en respuesta a una prop, aunque también reporta el efecto 4, como muestra la ejecución del linter anterior. Cuando solo debe reiniciarse una parte del estado, la solución suele ser reestructurarlo o calcular el valor durante el render.
Falsos positivos y efectos que se quedan
Los efectos que conectan un componente con algo que React no gestiona (temporizadores, APIs del DOM, widgets ajenos a React, conexiones de red) son precisamente aquello para lo que se creó useEffect, así que déjalos donde están. La mayoría necesita una función de limpieza (cleanup), como explica Synchronizing with Effects. Si una medición del DOM provoca un parpadeo visible, la elección entre useEffect y useLayoutEffect importa más que decidir si mantener el efecto.
Las suscripciones a un store externo son la excepción a la regla de conservar los efectos de sincronización. Su lugar es useSyncExternalStore, que lee una instantánea (snapshot) durante el render en vez de copiarla al estado desde un efecto. no-external-store-subscription señala la versión basada en efectos.
El análisis estático de efectos tiene casos límite. La propia regla set-state-in-effect de React advierte cuando un efecto llama a un setter de estado de forma síncrona, y un issue de React sobre la rigurosidad de set-state-in-effect cuestiona lo estricta que es con los flags de montaje y los patrones de detección del cliente. Trata las advertencias sobre esos patrones como una invitación a revisar el código, no como un veredicto en uno u otro sentido. Tiene sentido ejecutar ambos plugins: la regla de React cubre setState dentro de efectos y este plugin añade los callbacks al padre, las actualizaciones encadenadas y las suscripciones a stores.
Antes de suprimir una advertencia, pregúntate:
- ¿El efecto se comunica con un sistema externo a React?
- ¿Necesita limpieza cuando cambian las dependencias o se desmonta el componente?
- ¿Moverlo al render o a un manejador cambiaría un comportamiento visible para los usuarios?
Si las respuestas justifican conservarlo, desactiva la regla en esa línea y usa la sintaxis de descripción -- de ESLint para dejar constancia del motivo:
// eslint-disable-next-line react-you-might-not-need-an-effect/no-initialize-state -- client-only value, avoids hydration mismatch
Este prefijo coincide con el de los presets, Oxlint y las configuraciones heredadas. Usa reactYouMightNotNeedAnEffect/ solo si registraste tú mismo el plugin con esa clave.
Cómo implantarlo
Implanta eslint-plugin-react-you-might-not-need-an-effect primero con recommended, para que los hallazgos aparezcan como advertencias y la CI siga pasando. Revísalos componente a componente, refactorizando o anotando cada uno, y cambia a strict cuando el número de advertencias llegue a cero, de modo que cualquier nuevo efecto innecesario haga fallar el build.
Preguntas frecuentes
¿Puedo usar eslint-plugin-react-you-might-not-need-an-effect con una configuración heredada .eslintrc?
Sí. Los proyectos con el formato heredado .eslintrc extienden plugin:react-you-might-not-need-an-effect/legacy-recommended para reportar todas las reglas como advertencias, o plugin:react-you-might-not-need-an-effect/legacy-strict para reportarlas como errores. En las configuraciones heredadas, los IDs de las reglas y los comentarios de desactivación usan el prefijo en kebab-case react-you-might-not-need-an-effect/, el mismo que emplean los presets de flat config.
¿Cuándo debo envolver un valor derivado en useMemo en lugar de calcularlo en cada render?
Usa useMemo solo cuando el cálculo sea lento de forma medible. La documentación de React sugiere cronometrarlo con console.time y memoizarlo si el total ronda 1 ms o más. useMemo almacena en caché el resultado entre renders sin llevarlo al estado, por lo que sigue siendo correcto desde el primer render. React Compiler puede memoizar muchos cálculos automáticamente, lo que hace que las llamadas manuales a useMemo para valores derivados sean necesarias con menos frecuencia.
¿Cambiar la key de un componente también reinicia el estado de sus hijos?
Sí. Cuando cambia la key, React desmonta el componente y todo su subárbol y, a continuación, monta instancias nuevas. Todos los hijos pierden su estado local, los nodos del DOM se recrean y todos los efectos ejecutan su limpieza y vuelven a ejecutarse. Si solo debe reiniciarse una parte del estado y el de los hijos tiene que conservarse, ajusta ese estado directamente o reestructúralo en lugar de usar key.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k