learning-front

Nivel 6 · React de cero a héroe (con TypeScript)

Gestión de estado: Zustand y Redux Toolkit

Cuándo el estado local y el context no bastan, qué resuelve un store global y cómo se usa Zustand en la práctica.

El problema que resuelves con un store global#

A lo largo del Nivel 6 has aprendido varias formas de gestionar estado:

  • useState para valores simples dentro de un componente.
  • Levantar el estado” para compartirlo entre hermanos.
  • Context para evitar el prop drilling cuando un dato necesita llegar hondo.
  • useReducer para centralizar la lógica cuando hay muchas transiciones relacionadas.

Todas estas herramientas tienen algo en común: el estado sigue dentro del árbol de React. Alguien es el dueño, lo crea con useState o useReducer, y el resto lo recibe por props o lo lee por context.

Esto funciona en la mayoría de apps. Pero hay un punto en el que empieza a ser difícil de escalar: cuando muchos componentes lejanos del árbol necesitan leer y mutar el mismo estado, y quieres que ese estado tenga una única fuente de verdad sin depender de la jerarquía.

Un store global resuelve exactamente eso: el estado vive fuera del árbol, accesible desde cualquier componente sin Provider ni props.

Cuándo NO lo necesitas (esto es lo importante)#

Antes de hablar de stores, el aviso más honesto es este: la mayoría de las apps no los necesitan.

Si el estado solo interesa a un componenteuseState local.

Si dos o tres componentes cercanos comparten el dato → levanta el estado y pasa props.

Si el dato necesita llegar hondo pero no se muta mucho → context.

Un store global gana cuando:

  • Muchos componentes lejanos entre sí leen y mutan el mismo estado.
  • Quieres una única fuente de verdad fuera del árbol (no atada a ningún componente).
  • El context se queda corto porque re-renderiza demasiado (lo ves a continuación).

Abusar del estado global acopla todo y hace difícil saber quién cambia qué. Es la herramienta que se saca cuando las anteriores no bastan.

Context tiene un límite: todos re-renderizan#

Recuerda cómo funciona context: el Provider envuelve el árbol con un value, y cuando ese value cambia, todos los componentes que llaman a useContext se re-renderizan.

Eso es por diseño. El problema es que no puedes suscribirte a una porción del value. Si el contexto tiene { equipo, favoritos, filtros } y solo cambian los favoritos, todos los componentes que usen useContext re-renderizan aunque solo necesiten el equipo.

El demo de abajo muestra el Team Builder con context: funciona, pero por dentro, al añadir un héroe, los tres HeroCard se re-renderizan aunque su contenido no cambie. No se aprecia a simple vista, pero es justo lo que el demo siguiente, con Zustand y un selector, evita.

Puedes mitigarlo con useMemo en el value del Provider, pero eso es añadir complejidad manual para esquivar una limitación de diseño.

Zustand: un store sin Provider#

Zustand es una librería de estado global para React. La idea central es sencilla:

  1. Llamas a create() pasando una función que describe el estado inicial y las acciones.
  2. create() te devuelve un hook (por convención, useMiStore).
  3. Cualquier componente llama a ese hook para leer el estado o las acciones.
  4. No hay Provider. No hay boilerplate. El store vive fuera del árbol.
tsx
import { create } from "zustand";

// Describe la forma del store: qué datos y qué acciones tiene.
interface EquipoStore {
  equipo: Heroe[];
  añadir: (hero: Heroe) => void;
  quitar: (id: number) => void;
}

// create() recibe una función que recibe set y devuelve el objeto del store.
const useEquipoStore = create<EquipoStore>((set) => ({
  // Estado inicial.
  equipo: [],
  // Acción: set recibe el estado actual y devuelve los cambios (como setState).
  añadir: (hero) => set((s) => ({ equipo: [...s.equipo, hero] })),
  // Acción: filtramos al héroe por id.
  quitar: (id) => set((s) => ({ equipo: s.equipo.filter((h) => h.id !== id) })),
}));

Desde cualquier componente:

tsx
// Sin selector: el componente se suscribe a todo el store.
function MiComponente() {
  // Desestructuramos lo que necesitamos del hook.
  const { equipo, añadir } = useEquipoStore();
  // ...
}

Selectores: suscribirse a lo que importa#

La ventaja real de Zustand sobre context está en los selectores. En vez de leer todo el store, le dices exactamente qué porción necesitas:

tsx
// Selector: "dame solo si este héroe está en el equipo".
// Si otro héroe se añade, este valor no cambia → el componente no re-renderiza.
const enEquipo = useEquipoStore((s) => s.equipo.some((h) => h.id === hero.id));

Zustand ejecuta el selector en cada cambio de store y solo re-renderiza si el valor devuelto cambia. Con context no puedes hacer esto sin useMemo manual.

Pruébalo#

El demo de abajo muestra el mismo Team Builder pero con Zustand. Compara el código con el de context: no hay Provider, el store es global, y cada HeroCard se suscribe solo a si su héroe está en el equipo.

Observa el patrón de las acciones: set((s) => ({ equipo: [...s.equipo, hero] })). Zustand solo necesita el fragmento del estado que cambia; el resto lo fusiona solo. Y las acciones viven junto al estado, en el store, no repartidas por los componentes.

Zustand vs Redux Toolkit#

Zustand no es la única opción. Redux Toolkit (RTK) es la otra grande, y es importante conocerla porque la encontrarás en muchos proyectos.

La diferencia no es que uno sea mejor que el otro: es que resuelven el mismo problema con filosofías distintas.

Redux Toolkit: más estructura, más trazabilidad#

RTK organiza el estado en slices. Un slice es un bloque que agrupa el nombre, el estado inicial y los reducers de una porción del estado global:

typescript
import { createSlice, configureStore } from "@reduxjs/toolkit";

// createSlice genera el reducer y los action creators automáticamente.
const equipoSlice = createSlice({
  // El nombre identifica el slice en Redux DevTools.
  name: "equipo",
  // Estado inicial de esta porción del store.
  initialState: { lista: [] as Heroe[] },
  reducers: {
    // Cada clave es una acción; RTK permite mutar el estado aquí
    // porque usa Immer por dentro para producir el objeto nuevo.
    añadir(state, action) {
      state.lista.push(action.payload);
    },
    quitar(state, action) {
      state.lista = state.lista.filter((h) => h.id !== action.payload);
    },
  },
});

// configureStore crea el store global combinando todos los slices.
const store = configureStore({
  reducer: { equipo: equipoSlice.reducer },
});

En los componentes se usa con useSelector y useDispatch de react-redux, y hay que envolver la app en un <Provider store={store}>.

El trade-off honesto#

ZustandRedux Toolkit
BoilerplateMínimo (un create())Más estructura (slices, Provider, configureStore)
ProviderNo hace faltaNecesario
DevToolsPlugin disponibleIntegrado y muy potente
FilosofíaLibre: defines el store como quierasConvenciones claras
Cuándo brillaApps medianas, proyectos ágiles, equipos pequeñosApps grandes, muchos devs, necesidad de trazabilidad

Ni Zustand ni RTK es “el correcto”. La elección depende del tamaño del proyecto y las necesidades del equipo. En 2026, Zustand se usa mucho en proyectos nuevos por su simpleza; RTK sigue siendo el estándar en aplicaciones de empresa grandes con historial.

El patrón Flux#

Tanto Zustand como Redux siguen el mismo patrón de arquitectura de datos: Flux.

La idea es sencilla: los datos fluyen en una sola dirección.

texto
Componente → dispara acción → store la procesa → estado nuevo → componente re-renderiza

Esto hace el estado predecible: dado un estado y una acción, siempre sabes cuál será el estado siguiente. No hay mutaciones ocultas, no hay efectos secundarios en la actualización. Es la misma filosofía que useReducer, pero aplicada a un store fuera del árbol.

Comprueba lo que sabes#

Pregunta 1 de 4

¿Cuándo tiene sentido sacar el estado a un store global?

Tu turno#

Ejercicio · en esta página

Mueve el equipo a un store de Zustand

El Team Builder hasta ahora gestionaba el equipo con useReducer. Aquí lo sacas a un store global de Zustand: cualquier componente puede leer y mutar el equipo sin props ni Provider.

Paso 1: Store básico con Zustand

  • create() define el store con equipo (array), añadir, quitar y vaciar
  • El hook del store se usa desde el componente para leer el estado y las acciones
  • Los tres botones (añadir, quitar, vaciar) funcionan correctamente
Ver soluciones
import { create } from "zustand";

interface Heroe {
  id: number;
  nombre: string;
  rol: string;
  partidas: number;
  victorias: number;
}

const ROSTER: Heroe[] = [
  { id: 1, nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
  { id: 2, nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 150 },
  { id: 3, nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 52 },
  { id: 4, nombre: "Ana", rol: "Apoyo", partidas: 88, victorias: 61 },
  { id: 5, nombre: "Genji", rol: "Daño", partidas: 140, victorias: 79 },
  { id: 6, nombre: "Sigma", rol: "Tanque", partidas: 70, victorias: 41 },
];

// El store agrupa estado y acciones en un solo objeto.
// create() devuelve el hook con el que cualquier componente accede al store.
interface EquipoStore {
  equipo: Heroe[];
  // Las acciones reciben set para actualizar el estado.
  añadir: (hero: Heroe) => void;
  quitar: (id: number) => void;
  vaciar: () => void;
}

// create() crea el store; set es la función para actualizar el estado.
const useEquipoStore = create<EquipoStore>((set) => ({
  // Estado inicial: equipo vacío.
  equipo: [],
  // Acción añadir: nuevo array con el héroe añadido.
  añadir: (hero) =>
    set((s) => ({ equipo: [...s.equipo, hero] })),
  // Acción quitar: filtramos al héroe por id.
  quitar: (id) =>
    set((s) => ({ equipo: s.equipo.filter((h) => h.id !== id) })),
  // Acción vaciar: equipo vacío.
  vaciar: () => set({ equipo: [] }),
}));

export default function App() {
  // Leemos TODO el store: si cualquier campo cambia, este componente re-renderiza.
  const { equipo, añadir, quitar, vaciar } = useEquipoStore();

  return (
    <section>
      <h1 className="titulo">Team Builder</h1>

      <div className="seccion">
        <h2 className="seccion__titulo">Roster</h2>
        <div className="lista">
          {ROSTER.map((hero) => (
            <article key={hero.id} className="tarjeta">
              <h3 className="tarjeta__nombre">{hero.nombre}</h3>
              <p className="tarjeta__rol">{hero.rol}</p>
              <button
                className="boton"
                onClick={() => añadir(hero)}
              >
                Añadir
              </button>
            </article>
          ))}
        </div>
      </div>

      <div className="seccion">
        <h2 className="seccion__titulo">Tu equipo ({equipo.length}/6)</h2>
        {equipo.length === 0 ? (
          <p className="equipo-vacio">Aún no has añadido héroes.</p>
        ) : (
          <div className="lista">
            {equipo.map((hero) => (
              <article key={hero.id} className="tarjeta">
                <h3 className="tarjeta__nombre">{hero.nombre}</h3>
                <p className="tarjeta__rol">{hero.rol}</p>
                <button
                  className="boton boton--quitar"
                  onClick={() => quitar(hero.id)}
                >
                  Quitar
                </button>
              </article>
            ))}
          </div>
        )}
        {equipo.length > 0 && (
          <button className="boton boton--quitar" onClick={vaciar}>
            Vaciar equipo
          </button>
        )}
      </div>
    </section>
  );
}

Por qué este nivel

  • create() agrupa estado y acciones: sin Provider, sin boilerplate. Eso ya es una mejora clara sobre context para estado que muchos componentes necesitan.
  • El hook del store se usa en App directamente: no hay un 'dueño' del estado en el árbol. Cualquier componente nuevo puede conectarse al store sin que nadie tenga que pasarle nada.