Saltar al contenido principal

Estado Global — Redux Toolkit & Zustand 🟡

¿Cuándo necesitas estado global?

✅ Usar estado global cuando:
- El estado se comparte entre componentes no relacionados
- Hay mucho prop drilling (pasar props 3+ niveles)
- El estado persiste entre rutas/páginas
- Lógica de sincronización compleja (auth, cart, notifications)

❌ No usar estado global para:
- Estado de un solo componente (useState es suficiente)
- Datos del servidor → usar React Query / SWR
- Estado de UI simple (abrir/cerrar modal)

Redux Toolkit (RTK)

Redux moderno — elimina el boilerplate del Redux clásico.

Store y Slices

// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import carritoReducer from './carritoSlice';
import authReducer from './authSlice';

export const store = configureStore({
reducer: {
carrito: carritoReducer,
auth: authReducer,
},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// store/carritoSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface Item {
id: number;
nombre: string;
precio: number;
cantidad: number;
}

interface CarritoState {
items: Item[];
descuento: number;
}

const initialState: CarritoState = {
items: [],
descuento: 0,
};

const carritoSlice = createSlice({
name: 'carrito',
initialState,
reducers: {
agregarItem(state, action: PayloadAction<Omit<Item, 'cantidad'>>) {
const existente = state.items.find(i => i.id === action.payload.id);
if (existente) {
existente.cantidad++; // RTK usa Immer internamente → mutación segura
} else {
state.items.push({ ...action.payload, cantidad: 1 });
}
},
quitarItem(state, action: PayloadAction<number>) {
state.items = state.items.filter(i => i.id !== action.payload);
},
vaciarCarrito(state) {
state.items = [];
},
aplicarDescuento(state, action: PayloadAction<number>) {
state.descuento = action.payload;
},
},
});

export const { agregarItem, quitarItem, vaciarCarrito, aplicarDescuento } =
carritoSlice.actions;

// Selectors
export const selectItems = (state: RootState) => state.carrito.items;
export const selectTotal = (state: RootState) =>
state.carrito.items.reduce((acc, i) => acc + i.precio * i.cantidad, 0) *
(1 - state.carrito.descuento / 100);
export const selectCantidadItems = (state: RootState) =>
state.carrito.items.reduce((acc, i) => acc + i.cantidad, 0);

export default carritoSlice.reducer;

RTK Query — Estado del servidor con Redux

// store/productosApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

interface Producto {
id: number;
nombre: string;
precio: number;
}

export const productosApi = createApi({
reducerPath: 'productosApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
tagTypes: ['Producto'],
endpoints: (builder) => ({
getProductos: builder.query<Producto[], void>({
query: () => 'productos',
providesTags: ['Producto'],
}),
getProducto: builder.query<Producto, number>({
query: (id) => `productos/${id}`,
providesTags: (result, error, id) => [{ type: 'Producto', id }],
}),
crearProducto: builder.mutation<Producto, Omit<Producto, 'id'>>({
query: (body) => ({
url: 'productos',
method: 'POST',
body,
}),
invalidatesTags: ['Producto'], // refresca la lista
}),
}),
});

export const { useGetProductosQuery, useGetProductoQuery, useCrearProductoMutation } =
productosApi;

Uso en componentes

// Conectar con hooks tipados
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '../store';

// Custom hooks tipados (buena práctica)
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector = <T>(selector: (state: RootState) => T) =>
useSelector(selector);

// Componente
function CarritoIcono() {
const cantidad = useAppSelector(selectCantidadItems);
const dispatch = useAppDispatch();

return (
<button onClick={() => dispatch(vaciarCarrito())}>
Carrito ({cantidad})
</button>
);
}

Zustand — Estado global minimalista

Mucho más simple que Redux. Ideal para proyectos medianos.

// store/useCarritoStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware'; // persiste en localStorage

interface Item {
id: number;
nombre: string;
precio: number;
cantidad: number;
}

interface CarritoStore {
items: Item[];
agregarItem: (item: Omit<Item, 'cantidad'>) => void;
quitarItem: (id: number) => void;
vaciarCarrito: () => void;
total: () => number;
}

export const useCarritoStore = create<CarritoStore>()(
persist(
(set, get) => ({
items: [],

agregarItem: (item) =>
set((state) => {
const existente = state.items.find(i => i.id === item.id);
if (existente) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, cantidad: i.cantidad + 1 } : i
),
};
}
return { items: [...state.items, { ...item, cantidad: 1 }] };
}),

quitarItem: (id) =>
set((state) => ({
items: state.items.filter(i => i.id !== id),
})),

vaciarCarrito: () => set({ items: [] }),

total: () =>
get().items.reduce((acc, i) => acc + i.precio * i.cantidad, 0),
}),
{
name: 'carrito-storage', // key en localStorage
}
)
);

// Uso en componente (sin Provider!)
function Carrito() {
const { items, agregarItem, quitarItem, total } = useCarritoStore();

return (
<div>
<p>Total: ${total().toFixed(2)}</p>
{items.map(item => (
<div key={item.id}>
{item.nombre} x{item.cantidad}
<button onClick={() => quitarItem(item.id)}>X</button>
</div>
))}
</div>
);
}

Comparación: Redux Toolkit vs Zustand vs Context

CriterioRedux ToolkitZustandContext API
BoilerplateMedioMínimoNinguno
DevTools✅ Excelente✅ Bueno❌ Limitado
Performance✅ Alto✅ Alto⚠️ Re-renders
Curva de aprendizajeMedia-AltaBajaNinguna
Escalabilidad✅ Alta✅ Media❌ Baja
TypeScript✅ Excelente✅ Bueno✅ Bueno
Tamaño bundle~47KB~1KB0
Ideal paraApps grandes, equipos grandesApps medianasEstado simple

¿Cuándo usar cada uno?

React Query / SWR          → estado del servidor (datos de API)
useState / useReducer → estado local del componente
Context API → estado de UI liviano (tema, idioma)
Zustand → estado global en apps medianas
Redux Toolkit → apps grandes, equipos grandes, redux devtools