AL.
🇺🇸 EN
Volver al blog
JavaScript y React · 9 min de lectura

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:

  • fullName se deriva de firstName y lastName, 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:

  1. Click button cuando count = 0
  2. Después de 3s, setCount(0 + 1) ejecuta (aunque count ahora puede ser 5)
  3. count se 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:

  1. No dupliques estado — calcula valores derivados.
  2. No mutes estado — crea nuevas referencias.
  3. Usa refs para no-UI state — evita re-renders innecesarios.
  4. Usa enums sobre múltiples booleans — para estados mutuamente exclusivos.
  5. Usa useReducer para estado complejo — cuando múltiples valores cambian juntos.
  6. Evita closures rancias — usa la forma funcional de setState.
  7. Lazy initialization — para computación costosa.
  8. 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.