¡Código - Código!

useState → estado local en React

Herramienta: useState

Framework: React

Persona Encargada: Ariel GonzAgüer

Creado: 7-9-2025

Última actualización: 16-9-2025

Este artículo cubre todo lo que necesita saber sobre useState en React: desde lo básico hasta patrones avanzados.

¿Qué es useState?

useState es un hook fundamental de React que permite a los componentes funcionales tener estado local. Devuelve un array con dos elementos: el valor actual del estado y una función para actualizarlo.

import { useState } from 'react';

function Contador() {
const [count, setCount] = useState(0);

return (
<div>
<p>Contador: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}

Contador: 0

1. Sintaxis básica

Declaración y uso

import { useState } from 'react';

function MiComponente() {
  // [valorActual, funcionParaActualizar] = useState(valorInicial)
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div>
      <p>Contador: {count}</p>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Escribe tu nombre"
      />
      {isVisible && <p>¡Hola {name}!</p>}
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? 'Ocultar' : 'Mostrar'} saludo
      </button>
    </div>
  );
}

Convenciones de nomenclatura

2. Tipos de valores de estado

Primitivos

function EjemplosPrimitivos() {
  const [count, setCount] = useState(0);           // número
  const [message, setMessage] = useState('Hola');  // string
  const [isActive, setIsActive] = useState(false); // boolean
  const [data, setData] = useState(null);          // null/undefined
  
  return (
    <div>
      <p>Contador: {count}</p>
      <p>Mensaje: {message}</p>
      <p>Activo: {isActive ? 'Sí' : 'No'}</p>
      <p>Datos: {data ? 'Cargados' : 'Sin datos'}</p>
    </div>
  );
}

Objetos

function EjemploObjeto() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });
  
  // ❌ Incorrecto - modifica el estado directamente
  const updateNameWrong = () => {
    user.name = 'Juan'; // No funciona
  };
  
  // ✅ Correcto - crea nuevo objeto
  const updateName = () => {
    setUser({
      ...user,
      name: 'Juan'
    });
  };
  
  // ✅ Otra forma correcta con función
  const updateAge = () => {
    setUser(prevUser => ({
      ...prevUser,
      age: prevUser.age + 1
    }));
  };
  
  return (
    <div>
      <p>Nombre: {user.name}</p>
      <p>Email: {user.email}</p>
      <p>Edad: {user.age}</p>
      <button onClick={updateName}>Cambiar nombre</button>
      <button onClick={updateAge}>Incrementar edad</button>
    </div>
  );
}

Arrays

function ListaTareas() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState('');
  
  const addTask = () => {
    if (newTask.trim()) {
      // ✅ Agregar al final
      setTasks([...tasks, { id: Date.now(), text: newTask }]);
      setNewTask('');
    }
  };
  
  const removeTask = (id) => {
    // ✅ Filtrar elemento
    setTasks(tasks.filter(task => task.id !== id));
  };
  
  const updateTask = (id, newText) => {
    // ✅ Mapear y actualizar
    setTasks(tasks.map(task => 
      task.id === id ? { ...task, text: newText } : task
    ));
  };
  
  return (
    <div>
      <input 
        value={newTask}
        onChange={(e) => setNewTask(e.target.value)}
        placeholder="Nueva tarea"
      />
      <button onClick={addTask}>Agregar</button>
      
      <ul>
        {tasks.map(task => (
          <li key={task.id}>
            {task.text}
            <button onClick={() => removeTask(task.id)}>Eliminar</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

3. Actualizaciones funcionales

¿Por qué usar funciones?

Cuando el nuevo estado depende del estado anterior, use funciones para evitar problemas de concurrencia:

function Contador() {
  const [count, setCount] = useState(0);
  
  // ❌ Problemático con múltiples actualizaciones rápidas
  const incrementWrong = () => {
    setCount(count + 1);
    setCount(count + 1); // Ambas usan el mismo valor de count
  };
  
  // ✅ Correcto - usa el valor más reciente
  const incrementCorrect = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // Cada una usa el valor actualizado
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementWrong}>+2 (Incorrecto)</button>
      <button onClick={incrementCorrect}>+2 (Correcto)</button>
    </div>
  );
}

Patrones comunes

// Toggle boolean
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);

// Incrementar/decrementar
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);

// Agregar a array
const [items, setItems] = useState([]);
const addItem = (item) => setItems(prev => [...prev, item]);

// Resetear estado
const [form, setForm] = useState({ name: '', email: '' });
const resetForm = () => setForm({ name: '', email: '' });

4. Estado derivado y useEffect

Estado derivado

No almacene en estado lo que puede calcularse:

function ProductList() {
  const [products, setProducts] = useState([]);
  const [filter, setFilter] = useState('');

  // ✅ Estado derivado - se calcula en cada render
  const filteredProducts = products.filter(product =>
    product.name.toLowerCase().includes(filter.toLowerCase())
  );

  // ❌ No hacer esto - estado redundante
  // const [filteredProducts, setFilteredProducts] = useState([]);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filtrar productos..."
      />

      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}

Sincronización con useEffect

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

    fetchUser();

}, [userId]); // Se ejecuta cuando userId cambia

if (loading) return <div>Cargando...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>Usuario no encontrado</div>;

return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}

5. Patrones avanzados

Estado complejo con useReducer

Para estados complejos, considere useReducer:

import { useReducer } from 'react';

// Cuando useState se vuelve complejo
function TodoApp() {
  const [state, setState] = useState({
    todos: [],
    filter: 'all',
    loading: false,
    error: null
  });

  // Muchas funciones de actualización...
  const addTodo = (text) => {
    setState(prev => ({
      ...prev,
      todos: [...prev.todos, { id: Date.now(), text, completed: false }]
    }));
  };

  // ✅ Mejor con useReducer para lógica compleja
  // const [state, dispatch] = useReducer(todoReducer, initialState);
}

Estado compartido con Context

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  };

  return <ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme debe usarse dentro de ThemeProvider');
  }
  return context;
}

6. Optimización y rendimiento

Evitar renders innecesarios

import { useState, memo, useCallback, useMemo } from 'react';

const ExpensiveComponent = memo(({ data, onUpdate }) => {
console.log('ExpensiveComponent renderizado');
return <div>{/* contenido costoso */}</div>;
});

function Parent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);

// ✅ useCallback para funciones estables
const handleUpdate = useCallback((newItem) => {
setItems(prev => [...prev, newItem]);
}, []);

// ✅ useMemo para cálculos costosos
const expensiveData = useMemo(() => {
return items.filter(item => item.active).length;
}, [items]);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>

      {/* No se re-renderiza cuando count cambia */}
      <ExpensiveComponent
        data={expensiveData}
        onUpdate={handleUpdate}
      />
    </div>

);
}

7. Errores comunes

Mutación directa del estado

// ❌ NUNCA hacer esto
const [user, setUser] = useState({ name: 'Ana' });
user.name = 'Carlos'; // Mutación directa - NO funciona

const [items, setItems] = useState([1, 2, 3]);
items.push(4); // Mutación directa - NO funciona

// ✅ Hacer esto en su lugar
setUser({ ...user, name: 'Carlos' });
setItems([...items, 4]);

Estado desactualizado en closures

function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // ❌ Problemático - count queda "congelado"
    const interval = setInterval(() => {
      setCount(count + 1); // Siempre usa count = 0
    }, 1000);
    
    return () => clearInterval(interval);
  }, []); // Dependencias vacías
  
  useEffect(() => {
    // ✅ Correcto - usa función para obtener el valor actual
    const interval = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
    
    return () => clearInterval(interval);
  }, []); // Dependencias vacías OK con función
}

8. Testing

Pruebas básicas

import { render, fireEvent, screen } from '@testing-library/react';
import Contador from './Contador';

test('incrementa el contador al hacer click', () => {
render(<Contador />);

const button = screen.getByText('+1');
const counter = screen.getByText(/contador: 0/i);

fireEvent.click(button);

expect(screen.getByText(/contador: 1/i)).toBeInTheDocument();
});

test('actualiza el input correctamente', () => {
render(<FormularioNombre />);

const input = screen.getByPlaceholderText('Escribe tu nombre');

fireEvent.change(input, { target: { value: 'Juan' } });

expect(input.value).toBe('Juan');
});

Resumen

useState es fundamental en React para:

La clave está en entender que React re-renderiza cuando el estado cambia, y que siempre debe tratar el estado como inmutable.