La Necesidad de la Gestión de Estado
A medida que las aplicaciones frontend se vuelven más complejas, compartir estado entre componentes se convierte en un desafío. La elección del patrón de gestión de estado adecuado impacta directamente en la mantenibilidad y escalabilidad de la aplicación.
flowchart TB
subgraph PropDrilling["Prop Drilling (El infierno del paso de props)"]
App["App<br/>state: { user, theme, cart }"]
Layout["Layout<br/>← props: user, theme"]
Header["Header<br/>← props: user, theme"]
Avatar["Avatar<br/>← props: user (finalmente se usa)"]
App --> Layout --> Header --> Avatar
end
Note["Problema: Los componentes intermedios solo pasan props innecesarios"]
Clasificación de Arquitecturas de Gestión de Estado
flowchart TB
subgraph Patterns["Clasificación de Patrones de Gestión de Estado"]
subgraph Flux["1. Patrón Flux/Redux"]
F1["Store único + Action + Reducer<br/>Redux, Zustand"]
end
subgraph Atomic["2. Patrón Atomic"]
A1["Unidades de estado pequeñas y distribuidas (Atom)<br/>Jotai, Recoil"]
end
subgraph Proxy["3. Patrón Proxy-based"]
P1["Seguimiento automático mediante Proxy<br/>Valtio, MobX"]
end
subgraph Signal["4. Patrón Signal"]
S1["Reactividad de grano fino<br/>Solid.js Signals, Preact Signals"]
end
end
Patrón Flux/Redux
Conceptos Básicos
flowchart LR
View["View"] -->|Action| Dispatcher["Dispatcher"]
Dispatcher -->|dispatch| Store["Store<br/>(Reducer)"]
Store -->|notify| State["State<br/>(solo lectura)"]
State --> View
Principios:
- Flujo de datos unidireccional
- El estado es de solo lectura (Inmutable)
- Los cambios solo se realizan a través de Actions
- Los Reducers son funciones puras
Implementación con Redux
// store/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface User {
id: string;
name: string;
email: string;
}
interface UserState {
currentUser: User | null;
isLoading: boolean;
error: string | null;
}
const initialState: UserState = {
currentUser: null,
isLoading: false,
error: null,
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
loginStart: (state) => {
state.isLoading = true;
state.error = null;
},
loginSuccess: (state, action: PayloadAction<User>) => {
state.currentUser = action.payload;
state.isLoading = false;
},
loginFailure: (state, action: PayloadAction<string>) => {
state.error = action.payload;
state.isLoading = false;
},
logout: (state) => {
state.currentUser = null;
},
},
});
export const { loginStart, loginSuccess, loginFailure, logout } = userSlice.actions;
export default userSlice.reducer;
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import cartReducer from './cartSlice';
export const store = configureStore({
reducer: {
user: userReducer,
cart: cartReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// hooks/useAppDispatch.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from '../store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// components/UserProfile.tsx
function UserProfile() {
const { currentUser, isLoading, error } = useAppSelector((state) => state.user);
const dispatch = useAppDispatch();
const handleLogout = () => {
dispatch(logout());
};
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!currentUser) return <div>Please login</div>;
return (
<div>
<h1>{currentUser.name}</h1>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
Implementación con Zustand
// store/useStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface User {
id: string;
name: string;
email: string;
}
interface UserStore {
// State
currentUser: User | null;
isLoading: boolean;
error: string | null;
// Actions
login: (email: string, password: string) => Promise<void>;
logout: () => void;
updateProfile: (updates: Partial<User>) => void;
}
export const useUserStore = create<UserStore>()(
devtools(
persist(
immer((set, get) => ({
currentUser: null,
isLoading: false,
error: null,
login: async (email, password) => {
set((state) => {
state.isLoading = true;
state.error = null;
});
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const user = await response.json();
set((state) => {
state.currentUser = user;
state.isLoading = false;
});
} catch (error) {
set((state) => {
state.error = error.message;
state.isLoading = false;
});
}
},
logout: () => {
set((state) => {
state.currentUser = null;
});
},
updateProfile: (updates) => {
set((state) => {
if (state.currentUser) {
Object.assign(state.currentUser, updates);
}
});
},
})),
{ name: 'user-storage' }
),
{ name: 'UserStore' }
)
);
// Selector (optimización de rendimiento)
export const useCurrentUser = () => useUserStore((state) => state.currentUser);
export const useIsLoading = () => useUserStore((state) => state.isLoading);
// components/UserProfile.tsx
function UserProfile() {
const currentUser = useCurrentUser();
const logout = useUserStore((state) => state.logout);
if (!currentUser) return null;
return (
<div>
<h1>{currentUser.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
Patrón Atomic
Implementación con Jotai
// atoms/userAtoms.ts
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
interface User {
id: string;
name: string;
email: string;
}
// Atom básico
export const currentUserAtom = atom<User | null>(null);
// Atom persistido
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
// Atom derivado (solo lectura)
export const isLoggedInAtom = atom((get) => get(currentUserAtom) !== null);
// Atom derivado (lectura/escritura)
export const userNameAtom = atom(
(get) => get(currentUserAtom)?.name ?? 'Guest',
(get, set, newName: string) => {
const user = get(currentUserAtom);
if (user) {
set(currentUserAtom, { ...user, name: newName });
}
}
);
// Atom asíncrono
export const userProfileAtom = atom(async (get) => {
const user = get(currentUserAtom);
if (!user) return null;
const response = await fetch(`/api/users/${user.id}/profile`);
return response.json();
});
// Atom de acción
export const loginAtom = atom(
null,
async (get, set, { email, password }: { email: string; password: string }) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const user = await response.json();
set(currentUserAtom, user);
}
);
export const logoutAtom = atom(null, (get, set) => {
set(currentUserAtom, null);
});
// components/UserProfile.tsx
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
function UserProfile() {
const currentUser = useAtomValue(currentUserAtom);
const [userName, setUserName] = useAtom(userNameAtom);
const logout = useSetAtom(logoutAtom);
if (!currentUser) return null;
return (
<div>
<input
value={userName}
onChange={(e) => setUserName(e.target.value)}
/>
<button onClick={logout}>Logout</button>
</div>
);
}
Implementación con Recoil
// atoms/userState.ts
import { atom, selector, selectorFamily } from 'recoil';
interface User {
id: string;
name: string;
email: string;
}
// Atom
export const currentUserState = atom<User | null>({
key: 'currentUser',
default: null,
});
// Selector (estado derivado)
export const isLoggedInState = selector({
key: 'isLoggedIn',
get: ({ get }) => get(currentUserState) !== null,
});
// Selector asíncrono
export const userProfileState = selector({
key: 'userProfile',
get: async ({ get }) => {
const user = get(currentUserState);
if (!user) return null;
const response = await fetch(`/api/users/${user.id}/profile`);
return response.json();
},
});
// Selector con parámetros (SelectorFamily)
export const userPostsState = selectorFamily({
key: 'userPosts',
get: (userId: string) => async () => {
const response = await fetch(`/api/users/${userId}/posts`);
return response.json();
},
});
// components/UserProfile.tsx
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';
function UserProfile() {
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
const isLoggedIn = useRecoilValue(isLoggedInState);
const profile = useRecoilValue(userProfileState);
const logout = () => setCurrentUser(null);
if (!isLoggedIn) return <div>Please login</div>;
return (
<div>
<h1>{currentUser?.name}</h1>
<p>{profile?.bio}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
flowchart TB
subgraph FluxPattern["Flux (Redux/Zustand)"]
Store["Single Store<br/>user | cart | ui | ..."]
Store --> CA["Component A"]
Store --> CB["Component B"]
Store --> CC["Component C"]
end
subgraph AtomicPattern["Atomic (Jotai/Recoil)"]
A1["Atom"] --> CompA["Component A"]
A2["Atom"] --> Derived["Derived Atom"]
A3["Atom"] --> Derived
Derived --> CompB["Component B"]
A4["Atom"] --> CompC["Component C"]
end
Características:
- Flux: Centralizado, predecible, fácil de depurar
- Atomic: Distribuido, grano fino, adecuado para code splitting
Patrón Proxy-based
Implementación con Valtio
// store/userStore.ts
import { proxy, useSnapshot } from 'valtio';
import { devtools } from 'valtio/utils';
interface User {
id: string;
name: string;
email: string;
}
interface UserStore {
currentUser: User | null;
isLoading: boolean;
error: string | null;
}
export const userStore = proxy<UserStore>({
currentUser: null,
isLoading: false,
error: null,
});
// Habilitar DevTools
devtools(userStore, { name: 'userStore' });
// Acciones (modificación directa posible)
export const userActions = {
async login(email: string, password: string) {
userStore.isLoading = true;
userStore.error = null;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
userStore.currentUser = await response.json();
} catch (error) {
userStore.error = error.message;
} finally {
userStore.isLoading = false;
}
},
logout() {
userStore.currentUser = null;
},
updateName(name: string) {
if (userStore.currentUser) {
userStore.currentUser.name = name; // Modificación directa posible
}
},
};
// components/UserProfile.tsx
function UserProfile() {
// Obtener snapshot de solo lectura con useSnapshot
const snap = useSnapshot(userStore);
if (snap.isLoading) return <div>Loading...</div>;
if (!snap.currentUser) return null;
return (
<div>
<input
value={snap.currentUser.name}
onChange={(e) => userActions.updateName(e.target.value)}
/>
<button onClick={userActions.logout}>Logout</button>
</div>
);
}
Comparación de Bibliotecas de Gestión de Estado
| Característica | Redux Toolkit | Zustand | Jotai | Recoil | Valtio |
|---|---|---|---|---|---|
| Tamaño del bundle | Grande | Pequeño | Pequeño | Medio | Pequeño |
| Curva de aprendizaje | Alta | Baja | Baja | Media | Baja |
| Boilerplate | Mucho | Poco | Poco | Medio | Poco |
| DevTools | Completo | Disponible | Disponible | Disponible | Disponible |
| TypeScript | Bueno | Excelente | Excelente | Bueno | Excelente |
| Soporte SSR | Bueno | Bueno | Excelente | Complejo | Bueno |
| Uso fuera de React | Posible | Posible | No | No | Posible |
Criterios de Selección
Guía de Selección de Bibliotecas de Gestión de Estado
Escala del proyecto y equipo:
| Escala | Recomendado | Razón |
|---|---|---|
| Grande, muchas personas | Redux Toolkit | Convenciones claras, ecosistema rico |
| Mediano, pocas personas | Zustand | Simple, bajo costo de aprendizaje |
| Pequeño, prototipo | Jotai/Valtio | Configuración mínima |
Requisitos de arquitectura:
| Requisito | Recomendado |
|---|---|
| Prioridad en predictibilidad | Redux/Zustand |
| Actualizaciones de grano fino | Jotai/Recoil |
| Operaciones mutables | Valtio |
| Server Components | Jotai (óptimo) |
Mejores Prácticas
// 1. Normalización del estado
interface NormalizedState {
users: {
byId: Record<string, User>;
allIds: string[];
};
posts: {
byId: Record<string, Post>;
allIds: string[];
};
}
// 2. Aprovechamiento del estado derivado
const selectUserPosts = (state: RootState, userId: string) =>
state.posts.allIds
.map(id => state.posts.byId[id])
.filter(post => post.authorId === userId);
// 3. Separación del procesamiento asíncrono
// Estado del servidor con TanStack Query / SWR
// Estado del cliente con Zustand / Jotai
// 4. División en granularidad apropiada
// NG: Un solo store gigante
// OK: Stores divididos por funcionalidad