Anti-Patrones de useState en React Que Todo Reviewer Deberia Detectar
De estado duplicado a hooks rotos — como detectar y corregir los errores mas comunes de useState en pull requests de React.
useState es el hook más usado en React, y también el más mal usado. Parece simple en la superficie, pero los desarrolladores frecuentemente caen en patrones que causan bugs, renders innecesarios y código difícil de mantener.
Como reviewer de código, ver estos anti-patrones es una señal de alerta. Como desarrollador, entenderlos te ahorrará horas de debugging. En este artículo, cubriremos los errores más comunes de useState que todos deberían detectar en code reviews.
1. Estado Duplicado / Derivado
Este es el anti-patrón más común. Los desarrolladores almacenan datos que pueden calcularse desde otro estado.
Mal Ejemplo
function UserProfile({ user }) {
const [firstName, setFirstName] = useState(user.firstName);
const [lastName, setLastName] = useState(user.lastName);
const [fullName, setFullName] = useState(`${user.firstName} ${user.lastName}`);
const handleFirstNameChange = (e) => {
const newFirstName = e.target.value;
setFirstName(newFirstName);
setFullName(`${newFirstName} ${lastName}`); // ¡Duplicación!
};
const handleLastNameChange = (e) => {
const newLastName = e.target.value;
setLastName(newLastName);
setFullName(`${firstName} ${newLastName}`); // ¡Duplicación!
};
return (
<div>
<input value={firstName} onChange={handleFirstNameChange} />
<input value={lastName} onChange={handleLastNameChange} />
<p>{fullName}</p>
</div>
);
}
Problema:
fullNamese deriva defirstNameylastName, no necesita estado.- Mantener sincronizados tres estados es propenso a errores.
- Si olvidas actualizar
fullName, se desincroniza.
Solución: Calcular Durante el Render
function UserProfile({ user }) {
const [firstName, setFirstName] = useState(user.firstName);
const [lastName, setLastName] = useState(user.lastName);
// Calcula durante render, sin estado extra
const fullName = `${firstName} ${lastName}`;
return (
<div>
<input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
<input value={lastName} onChange={(e) => setLastName(e.target.value)} />
<p>{fullName}</p>
</div>
);
}
Regla: Si un valor puede calcularse desde otro estado o props, no lo pongas en estado.
2. Actualizar Objetos/Arrays Incorrectamente (Mutación)
React depende de comparación de referencia para detectar cambios. Si mutas estado directamente, React no detectará el cambio.
Mal Ejemplo
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
todos.push({ id: Date.now(), text }); // ¡MUTACIÓN!
setTodos(todos); // Misma referencia, React no re-renderiza
};
return (
<div>
<button onClick={() => addTodo("New todo")}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Problema: todos.push() muta el array original. setTodos(todos) pasa la misma referencia de array, así que React piensa que nada cambió.
Solución: Crear un Nuevo Array
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text }]); // Nueva referencia
};
return (
<div>
<button onClick={() => addTodo("New todo")}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Para objetos:
// ❌ MAL
user.name = "John";
setUser(user);
// ✅ BIEN
setUser({ ...user, name: "John" });
Regla: Siempre crea nuevas referencias para objetos/arrays en estado. Usa spread (...), map, filter, etc.
3. Usar Estado Cuando Deberías Usar Ref
No todo necesita disparar un re-render. Si estás almacenando un valor que no afecta la UI, usa useRef en lugar de useState.
Mal Ejemplo
function Timer() {
const [count, setCount] = useState(0);
const [intervalId, setIntervalId] = useState(null); // ❌ No necesita re-render
const start = () => {
const id = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
setIntervalId(id); // Re-render innecesario
};
const stop = () => {
clearInterval(intervalId);
setIntervalId(null); // Re-render innecesario
};
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Problema: intervalId no afecta la UI. Cambiar su valor no debería causar un re-render.
Solución: Usar useRef
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null); // ✅ No causa re-renders
const start = () => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Regla: Usa useRef para valores que no afectan el render (IDs de timer, instancias de DOM, flags).
4. Múltiples Booleans en Lugar de un Enum/String
Cuando tienes estados mutuamente exclusivos, usar múltiples booleans es propenso a errores.
Mal Ejemplo
function Form() {
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
const submit = async () => {
setIsLoading(true);
setIsSuccess(false);
setIsError(false);
try {
await api.submit();
setIsLoading(false);
setIsSuccess(true);
} catch {
setIsLoading(false);
setIsError(true);
}
};
return (
<div>
{isLoading && <p>Loading...</p>}
{isSuccess && <p>Success!</p>}
{isError && <p>Error!</p>}
<button onClick={submit}>Submit</button>
</div>
);
}
Problema:
- Tres booleans para representar un solo estado (mutuamente exclusivo).
- Fácil olvidar resetear uno, causando bugs (ej. mostrar “Loading” y “Success” al mismo tiempo).
Solución: Usar un Enum
function Form() {
const [status, setStatus] = useState('idle'); // 'idle' | 'loading' | 'success' | 'error'
const submit = async () => {
setStatus('loading');
try {
await api.submit();
setStatus('success');
} catch {
setStatus('error');
}
};
return (
<div>
{status === 'loading' && <p>Loading...</p>}
{status === 'success' && <p>Success!</p>}
{status === 'error' && <p>Error!</p>}
<button onClick={submit} disabled={status === 'loading'}>
Submit
</button>
</div>
);
}
Regla: Para estados mutuamente exclusivos, usa un string/enum en lugar de múltiples booleans.
5. Múltiples Estados Relacionados en Lugar de useReducer
Cuando múltiples piezas de estado siempre cambian juntas, useReducer es más limpio.
Mal Ejemplo
function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const reset = () => {
setName('');
setEmail('');
setAge(0);
};
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={age} onChange={(e) => setAge(Number(e.target.value))} />
<button onClick={reset}>Reset</button>
</div>
);
}
Problema: Tres llamadas useState, tres setters, difícil gestionar lógica relacionada.
Solución: useReducer
const initialState = { name: '', email: '', age: 0 };
function reducer(state, action) {
switch (action.type) {
case 'update':
return { ...state, [action.field]: action.value };
case 'reset':
return initialState;
default:
return state;
}
}
function UserForm() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<input
value={state.name}
onChange={(e) => dispatch({ type: 'update', field: 'name', value: e.target.value })}
/>
<input
value={state.email}
onChange={(e) => dispatch({ type: 'update', field: 'email', value: e.target.value })}
/>
<input
value={state.age}
onChange={(e) => dispatch({ type: 'update', field: 'age', value: Number(e.target.value) })}
/>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
Regla: Cuando múltiples estados están relacionados o cambian juntos, considera useReducer.
6. Closures Rancias en Event Handlers
Este es sutil pero causa bugs confusos.
Mal Ejemplo
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1); // ¡Cierre rancio!
}, 3000);
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment after 3s</button>
</div>
);
}
Problema:
- Click button cuando
count = 0 - Después de 3s,
setCount(0 + 1)ejecuta (aunquecountahora puede ser 5) countse establece a 1 en lugar del valor esperado
El setTimeout captura el valor viejo de count.
Solución: Usar la Forma Funcional de setState
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount((prevCount) => prevCount + 1); // ✅ Siempre correcto
}, 3000);
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment after 3s</button>
</div>
);
}
Regla: Cuando el nuevo estado depende del viejo, usa la forma funcional: setState(prev => prev + 1).
7. Inicialización Costosa en useState
useState se ejecuta en cada render, incluso si solo necesitas inicializar una vez.
Mal Ejemplo
function DataTable() {
const [data, setData] = useState(expensiveCalculation()); // ¡Se ejecuta cada render!
return <Table data={data} />;
}
function expensiveCalculation() {
console.log('Computing...');
return Array.from({ length: 10000 }, (_, i) => ({ id: i, value: Math.random() }));
}
Problema: expensiveCalculation() corre en cada render, incluso si data nunca cambia.
Solución: Usar Lazy Initialization
function DataTable() {
const [data, setData] = useState(() => expensiveCalculation()); // ✅ Solo corre una vez
return <Table data={data} />;
}
Pasar una función a useState la ejecuta solo en el render inicial.
Regla: Para inicialización costosa, usa useState(() => expensiveComputation()).
8. Boolean Explosion
Demasiados booleans hacen el código difícil de razonar.
Mal Ejemplo
function Dashboard() {
const [showModal, setShowModal] = useState(false);
const [showSidebar, setShowSidebar] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [darkMode, setDarkMode] = useState(false);
const [isEditing, setIsEditing] = useState(false);
// ... muchos setters y lógica compleja
}
Problema: Difícil rastrear qué afecta qué. Fácil cometer errores.
Solución: Agrupar Estado Relacionado
function Dashboard() {
const [ui, setUi] = useState({
showModal: false,
showSidebar: false,
showTooltip: false,
darkMode: false,
isEditing: false,
});
const toggleModal = () => setUi((prev) => ({ ...prev, showModal: !prev.showModal }));
// ...
}
O usa múltiples useState si no están relacionados, pero agrupalos lógicamente.
Regla: Si tienes 5+ booleans, considera si algunos están relacionados y podrían agruparse.
Checklist de Code Review
Al revisar código React, busca:
- ✅ Estado derivado: ¿Puede calcularse desde otro estado/props?
- ✅ Mutación: ¿Se crean nuevos objetos/arrays en lugar de mutar?
- ✅ useState vs useRef: ¿El valor afecta el render?
- ✅ Múltiples booleans: ¿Deberían ser un enum?
- ✅ Estados relacionados: ¿Debería ser
useReducer? - ✅ Closures rancias: ¿Se usa la forma funcional de setState?
- ✅ Inicialización costosa: ¿Se usa lazy initialization?
- ✅ Boolean explosion: ¿Se puede agrupar estado?
Conclusión
useState es simple, pero usarlo correctamente requiere entender:
- No dupliques estado — calcula valores derivados.
- No mutes estado — crea nuevas referencias.
- Usa refs para no-UI state — evita re-renders innecesarios.
- Usa enums sobre múltiples booleans — para estados mutuamente exclusivos.
- Usa useReducer para estado complejo — cuando múltiples valores cambian juntos.
- Evita closures rancias — usa la forma funcional de setState.
- Lazy initialization — para computación costosa.
- Agrupa estado relacionado — evita boolean explosion.
Al entender estos anti-patrones, escribirás componentes React más limpios, rápidos y mantenibles — y detectarás problemas en code reviews antes de que lleguen a producción.