Como gestionar el estado en React de forma efectiva
Frontend

Como gestionar el estado en React de forma efectiva

6 October 2026 · 10 min de lectura
Home / Frontend / Como gestionar el estado en React de forma efectiva
Categorías:

Introduccion a la gestion de estado en React

La gestion de estado constituye una parte central de cualquier aplicacion React. El estado representa los datos que cambian a lo largo del tiempo y que determinan lo que se muestra en la interfaz. Cada vez que el estado se actualiza de forma correcta, React vuelve a renderizar los componentes afectados y mantiene la coherencia visual con los datos subyacentes.

En 2026 el ecosistema ofrece un conjunto consolidado de herramientas nativas y de terceros. La clave ya no es memorizar todas las librerias disponibles, sino clasificar correctamente el tipo de estado y aplicar la solucion mas simple que resuelva el problema. El estado local de un componente, el estado compartido de baja frecuencia, el estado global de cliente y el estado proveniente del servidor requieren enfoques distintos. Mezclar estos dominios en una unica solucion suele generar complejidad innecesaria y problemas de rendimiento.

Este articulo recorre las opciones principales, desde los hooks nativos hasta las librerias mas utilizadas, y proporciona criterios practicos para decidir cual emplear en cada situacion. El objetivo es construir una arquitectura clara, predecible y facil de mantener.

Estado local con useState

useState es el punto de partida para la gran mayoria de los casos. Permite declarar una variable de estado y una funcion para actualizarla dentro de un componente funcional. Cada llamada a la funcion de actualizacion provoca un nuevo render del componente y de sus hijos.

import { useState } from "react";

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

    return (
        <div>
            <p>Valor actual: {count}</p>
            <button onClick={() => setCount(count + 1)}>Incrementar</button>
            <button onClick={() => setCount((prev) => prev - 1)}>
                Decrementar
            </button>
        </div>
    );
}

La forma funcional de la actualizacion (prev => prev + 1) garantiza que se trabaje siempre con el valor mas reciente, lo que resulta especialmente util cuando se realizan multiples actualizaciones consecutivas o cuando la actualizacion depende del valor anterior.

useState es ideal para valores simples: booleans de visibilidad de modales, indices de pestanas, texto de campos controlados o contadores. Cuando el estado crece en complejidad o las transiciones se multiplican, conviene valorar useReducer.

useReducer para logica de actualizacion compleja

useReducer centraliza la logica de cambio de estado en una funcion reductora. Recibe el estado actual y una accion, y devuelve el nuevo estado. Este patron facilita el razonamiento sobre las transiciones posibles y permite probar la logica de forma independiente del componente.

import { useReducer } from "react";

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
    switch (action.type) {
        case "increment":
            return { ...state, count: state.count + state.step };
        case "decrement":
            return { ...state, count: state.count - state.step };
        case "setStep":
            return { ...state, step: action.payload };
        case "reset":
            return initialState;
        default:
            return state;
    }
}

function CounterWithReducer() {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <p>Contador: {state.count}</p>
            <input
                type="number"
                value={state.step}
                onChange={(e) =>
                    dispatch({
                        type: "setStep",
                        payload: Number(e.target.value),
                    })
                }
            />
            <button onClick={() => dispatch({ type: "increment" })}>+</button>
            <button onClick={() => dispatch({ type: "decrement" })}>-</button>
            <button onClick={() => dispatch({ type: "reset" })}>
                Reiniciar
            </button>
        </div>
    );
}

useReducer resulta especialmente util cuando varias acciones modifican el mismo conjunto de valores relacionados o cuando el siguiente estado depende de multiples campos. La explicitud de las acciones mejora la legibilidad y reduce errores de inconsistencia.

useMemo y useCallback para optimizacion

Aunque no almacenan estado mutable de la misma forma, useMemo y useCallback influyen en el rendimiento de los renders. useMemo memoriza el resultado de un calculo costoso y solo lo recalcula cuando cambian sus dependencias. useCallback memoriza una funcion para evitar que se recree en cada render, lo que es importante cuando se pasa como prop a componentes hijos memorizados.

import { useState, useMemo, useCallback } from "react";

function FilteredList({ items, filterText }) {
    const filtered = useMemo(() => {
        return items.filter((item) =>
            item.toLowerCase().includes(filterText.toLowerCase())
        );
    }, [items, filterText]);

    const handleClick = useCallback((id) => {
        console.log("Seleccionado:", id);
    }, []);

    return (
        <ul>
            {filtered.map((item) => (
                <li key={item} onClick={() => handleClick(item)}>
                    {item}
                </li>
            ))}
        </ul>
    );
}

El uso excesivo de estos hooks puede anadir complejidad sin beneficios reales. Conviene aplicarlos solo cuando se ha medido un problema de rendimiento o cuando se pasan funciones o valores derivados a componentes que dependen de la igualdad referencial.

useEffect para efectos secundarios

useEffect permite ejecutar codigo despues del render, tipicamente para sincronizar con sistemas externos: peticiones de red, suscripciones, temporizadores o manipulacion directa del DOM. El array de dependencias controla cuándo se vuelve a ejecutar el efecto.

import { useState, useEffect } from "react";

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);

    useEffect(() => {
        let cancelled = false;

        async function loadUser() {
            const response = await fetch(`/api/users/${userId}`);
            const data = await response.json();
            if (!cancelled) {
                setUser(data);
            }
        }

        loadUser();

        return () => {
            cancelled = true;
        };
    }, [userId]);

    if (!user) return <p>Cargando...</p>;
    return <div>{user.name}</div>;
}

La funcion de limpieza evita actualizaciones de estado en componentes ya desmontados y cancela suscripciones pendientes. Olvidar las dependencias o incluir objetos que cambian de identidad en cada render son fuentes habituales de bucles infinitos o de datos obsoletos.

useRef para valores mutables sin re-render

useRef devuelve un objeto mutable cuya propiedad current persiste entre renders sin provocar un nuevo render cuando cambia. Se utiliza para referencias a elementos del DOM, para almacenar valores previos o para contadores internos que no deben afectar a la interfaz.

import { useRef, useEffect } from "react";

function PreviousValue({ value }) {
    const prevRef = useRef();

    useEffect(() => {
        prevRef.current = value;
    }, [value]);

    return (
        <p>
            Actual: {value} — Anterior: {prevRef.current}
        </p>
    );
}

useRef no sustituye al estado cuando el valor debe reflejarse en la interfaz. Su papel es el de escape hatch para datos imperativos o para mantener referencias estables.

Context API para datos compartidos de baja frecuencia

Context permite compartir valores a lo largo del arbol de componentes sin necesidad de pasar props manualmente en cada nivel. Es adecuado para datos que cambian con poca frecuencia: tema visual, idioma, informacion basica del usuario autenticado o feature flags.

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

const ThemeContext = createContext(null);

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

    return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

function ThemedButton() {
    const { theme, setTheme } = useContext(ThemeContext);
    return (
        <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
            Tema actual: {theme}
        </button>
    );
}

El principal inconveniente de Context es que cualquier cambio en el valor del provider provoca un re-render de todos los consumidores, aunque solo utilicen una parte del valor. Para mitigar este problema se puede dividir el contexto en varios providers mas pequenos o combinarlo con selectores externos.

Zustand como almacen global ligero

Zustand ofrece un store global con una API minima basada en hooks. Los componentes se suscriben unicamente a las porciones de estado que necesitan mediante selectores, lo que evita re-renders innecesarios. Su tamano reducido y la ausencia de boilerplate lo han convertido en una de las opciones mas adoptadas para el estado de cliente en 2026.

import { create } from "zustand";

const useCartStore = create((set) => ({
    items: [],
    addItem: (item) => set((state) => ({ items: [...state.items, item] })),
    removeItem: (id) =>
        set((state) => ({
            items: state.items.filter((item) => item.id !== id),
        })),
    clear: () => set({ items: [] }),
}));

function CartSummary() {
    const itemCount = useCartStore((state) => state.items.length);
    return <p>Articulos en el carrito: {itemCount}</p>;
}

Zustand no impone un patron de acciones y reductores, aunque se puede adoptar voluntariamente. Es especialmente adecuado para estado de interfaz compartido, filtros, preferencias de usuario y carritos de compra.

TanStack Query para el estado del servidor

El estado que proviene de APIs remotas tiene un ciclo de vida distinto: caching, revalidacion, reintentos, estados de carga y error. TanStack Query (antes React Query) gestiona estos aspectos de forma declarativa y evita que el desarrollador tenga que reinventar la logica de sincronizacion.

import { useQuery } from "@tanstack/react-query";

function ProductList() {
    const { data, isLoading, error } = useQuery({
        queryKey: ["products"],
        queryFn: () => fetch("/api/products").then((res) => res.json()),
    });

    if (isLoading) return <p>Cargando productos...</p>;
    if (error) return <p>Error al cargar los datos</p>;

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

Separar el estado del servidor del estado de cliente es una de las decisiones arquitectonicas mas importantes. Intentar almacenar respuestas de API en un store global de cliente suele generar problemas de sincronizacion y de invalidacion de cache.

Redux Toolkit para flujos complejos y equipos grandes

Redux Toolkit simplifica el uso de Redux y sigue siendo una opcion solida cuando se necesita una estructura muy predecible, herramientas de depuracion avanzadas, middleware extensible o cuando varios equipos trabajan sobre el mismo estado. Su mayor peso y la curva de aprendizaje inicial lo hacen menos atractivo para proyectos pequenos o medianos que no requieren ese nivel de formalidad.

import { createSlice, configureStore } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";

const counterSlice = createSlice({
    name: "counter",
    initialState: { value: 0 },
    reducers: {
        increment: (state) => {
            state.value += 1;
        },
        decrement: (state) => {
            state.value -= 1;
        },
    },
});

const store = configureStore({
    reducer: { counter: counterSlice.reducer },
});

function Counter() {
    const value = useSelector((state) => state.counter.value);
    const dispatch = useDispatch();
    return (
        <div>
            <p>{value}</p>
            <button onClick={() => dispatch(counterSlice.actions.increment())}>
                +
            </button>
        </div>
    );
}

Redux Toolkit incluye RTK Query, que ofrece una alternativa integrada para el estado del servidor similar a TanStack Query.

Jotai y el modelo atomico

Jotai representa el estado como atomos independientes. Cada atomo puede leerse y escribirse de forma granular, y los atomos derivados se actualizan automaticamente cuando cambian sus dependencias. Este modelo brilla en interfaces muy interactivas donde distintas partes de la UI dependen de subconjuntos distintos de datos.

import { atom, useAtom } from "jotai";

const countAtom = atom(0);
const doubleAtom = atom((get) => get(countAtom) * 2);

function DoubleDisplay() {
    const [double] = useAtom(doubleAtom);
    return <p>El doble es: {double}</p>;
}

La flexibilidad de los atomos requiere disciplina para no dispersar el estado de forma dificil de rastrear. En aplicaciones con flujos de datos lineales, Zustand suele resultar mas sencillo de mantener.

Criterios practicos de decision

Una secuencia util para clasificar cada pieza de estado es la siguiente:

  1. Si el dato proviene de un servidor y necesita cache, revalidacion o reintentos, utilizar TanStack Query o equivalente.
  2. Si el valor debe ser reproducible mediante la URL (filtros, pestanas, paginacion), almacenarlo en los parametros de busqueda.
  3. Si solo lo necesita un componente o un subarbol reducido, mantenerlo con useState o useReducer.
  4. Si es un valor global de baja frecuencia (tema, locale, usuario basico), Context suele ser suficiente.
  5. Si es estado de cliente compartido y con actualizaciones frecuentes, Zustand ofrece un buen equilibrio.
  6. Si el dominio exige auditoria, flujos complejos y trabajo en equipo amplio, valorar Redux Toolkit.
  7. Si el modelo de datos se expresa de forma natural como grafo de atomos derivados, considerar Jotai.

Aplicar siempre la solucion mas simple que resuelva el problema evita la sobreingenieria y facilita la evolucion del codigo.

Errores frecuentes y como evitarlos

Uno de los errores mas comunes consiste en almacenar en el estado datos que pueden calcularse a partir de otros valores. El estado derivado debe obtenerse mediante useMemo o mediante selectores, no duplicarse. Otro fallo habitual es colocar respuestas de API dentro de un store de cliente sin una estrategia clara de invalidacion, lo que conduce a datos desactualizados.

Tambien se observa con frecuencia el uso de Context para estado que cambia a alta frecuencia, provocando re-renders masivos. En esos casos un store externo con selectores resuelve el problema de raiz. Finalmente, olvidar las funciones de limpieza en useEffect puede generar fugas de memoria o actualizaciones sobre componentes desmontados.

Conclusiones

La gestion de estado en React ha madurado hacia un enfoque pragmatico basado en la clasificacion del tipo de dato y en la eleccion de la herramienta minima necesaria. Los hooks nativos cubren la mayor parte de las necesidades locales. Context sigue siendo valioso para valores estables compartidos. Zustand se ha consolidado como la opcion por defecto para el estado global de cliente en la mayoria de los proyectos. TanStack Query gestiona de forma robusta el ciclo de vida de los datos remotos. Redux Toolkit y Jotai cubren escenarios mas especializados.

Dominar estos instrumentos y, sobre todo, saber cuando aplicar cada uno permite construir aplicaciones React predecibles, performantes y mantenibles. La pregunta correcta no es cual es la mejor libreria, sino que tipo de estado se esta gestionando y cual es la solucion mas simple que lo resuelve de forma adecuada.