Por Qué Dejé de Usar Redux en 2026 (Y Qué Uso Ahora)
Después de 4 años usando Redux en todos mis proyectos React, lo eliminé. Por qué Redux ya no tiene sentido para la mayoría de apps, y las alternativas que uso ahora con ejemplos reales.
Tabla de contenidos
Mi primer proyecto React serio tenía Redux. Mi segundo también. Y el tercero. Redux era la respuesta a todo: ¿necesitas estado global? Redux. ¿Fetch de datos? Redux + Thunk. ¿Formularios? Redux Form. ¿El estado del modal? Redux.
Tenía un store/ con 47 archivos entre slices, thunks, selectors, types y tests. Para una app de gestión de tareas.
Un día me senté a contar las líneas de código:
Lógica de negocio real: 2.400 líneas
Boilerplate de Redux: 3.100 líneas
Ratio señal/ruido: 43% señal, 57% ruido
Más de la mitad del código era fontanería de Redux. Ese día empecé la migración.
Qué me hizo abandonar Redux
1. El boilerplate no ha mejorado (aunque Redux Toolkit ayuda)
Redux Toolkit redujo mucho el boilerplate comparado con Redux vanilla. Pero sigue siendo demasiado:
// Para hacer un CRUD de tareas necesitas:
// 1. El slice (taskSlice.ts)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchTasks = createAsyncThunk(
'tasks/fetchAll',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/tasks');
return response.json();
} catch (err) {
return rejectWithValue(err.message);
}
}
);
const taskSlice = createSlice({
name: 'tasks',
initialState: { items: [], loading: false, error: null },
reducers: {
clearError: (state) => { state.error = null; },
},
extraReducers: (builder) => {
builder
.addCase(fetchTasks.pending, (state) => { state.loading = true; })
.addCase(fetchTasks.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTasks.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});
// 2. El store (store.ts)
// 3. Los selectors (taskSelectors.ts)
// 4. El Provider en el layout
// 5. Los types de RootState y AppDispatch
// 6. Los hooks tipados useAppSelector/useAppDispatch
~80 líneas de código para… hacer un fetch y guardar el resultado.
2. El 90% del “estado global” no necesita ser global
Hice una auditoría de mi store de Redux:
| Dato en el store | ¿Realmente necesita ser global? |
|---|---|
| Lista de tareas | No — solo la usa una página |
| Usuario logueado | Sí — se usa en muchos sitios |
| Estado de un modal | No — es local del componente |
| Datos de un formulario | No — es local del formulario |
| Filtros de búsqueda | No — pertenece a la URL |
| Tema dark/light | Sí — se usa globalmente |
| Notificaciones | Quizás — un Context bastaría |
De 12 slices en mi store, solo 2 necesitaban ser realmente globales. El resto era estado local o estado del servidor que debería venir de una caché de queries.
3. React Server Components cambiaron las reglas
Con Next.js App Router y React Server Components, muchos datos ya no pasan por el cliente:
// Antes (con Redux): fetch en el cliente → store → componente
// Ahora (con RSC): fetch en el servidor → componente directamente
// Este componente NO necesita Redux. Los datos llegan del servidor.
export default async function TaskList() {
const tasks = await db.task.findMany({
where: { userId: session.user.id }
});
return (
<ul>
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}
No hay loading state. No hay error state. No hay cache invalidation. El servidor hizo el fetch, renderizó el HTML, y el cliente lo recibe listo.
Qué uso ahora (y por qué)
Para datos del servidor: TanStack Query
// Antes: 80 líneas de Redux para un fetch
// Ahora: 5 líneas con TanStack Query
function TaskList() {
const { data: tasks, isLoading, error } = useQuery({
queryKey: ['tasks'],
queryFn: () => fetch('/api/tasks').then(r => r.json()),
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <ul>{tasks.map(t => <TaskItem key={t.id} task={t} />)}</ul>;
}
TanStack Query te da gratis: caché, deduplicación, refetch automático, optimistic updates, retry, y stale-while-revalidate. Todo lo que en Redux tenías que implementar a mano.
Para estado global simple: Zustand
// Antes: slice + store + provider + selectors + hooks tipados
// Ahora: un archivo de 10 líneas
import { create } from 'zustand';
interface AppStore {
theme: 'light' | 'dark';
toggleTheme: () => void;
user: User | null;
setUser: (user: User | null) => void;
}
export const useAppStore = create<AppStore>((set) => ({
theme: 'dark',
toggleTheme: () => set((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' })),
user: null,
setUser: (user) => set({ user }),
}));
// Uso: const theme = useAppStore(s => s.theme);
Sin Provider, sin boilerplate, con selectores automáticos, tipado perfecto. Para el 95% de necesidades de estado global, Zustand es todo lo que necesitas.
Para formularios: React Hook Form
// Antes: Redux Form (2.000 líneas de código para 5 formularios)
// Ahora: React Hook Form
const { register, handleSubmit, formState: { errors } } = useForm<TaskForm>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title', { required: 'El título es obligatorio' })} />
{errors.title && <span>{errors.title.message}</span>}
<button type="submit">Crear</button>
</form>
);
Para estado en la URL: nuqs (o useSearchParams)
Los filtros de búsqueda, la paginación, los tabs activos… todo eso pertenece a la URL, no al store:
import { useQueryState } from 'nuqs';
function TaskFilters() {
const [status, setStatus] = useQueryState('status', { defaultValue: 'all' });
const [page, setPage] = useQueryState('page', { defaultValue: '1' });
// URL: /tasks?status=pending&page=2
// Compartible, bookmarkeable, persistente al refrescar
}
La migración: Cómo eliminé Redux paso a paso
No lo hice de golpe. Fui slice por slice en 2 semanas:
Semana 1: Datos del servidor → TanStack Query
npm install @tanstack/react-query
- Identifiqué qué slices eran “estado del servidor” (datos que vienen de una API)
- Creé hooks con
useQueryyuseMutationpara cada uno - Eliminé los slices, thunks y selectors correspondientes
- Resultado: -1.800 líneas de código, misma funcionalidad
Semana 2: Estado local + global → Zustand + estado local
- Los modals, dropdowns y UI state →
useStatelocal en cada componente - El estado genuinamente global (tema, user, notificaciones) → un store de Zustand de 25 líneas
- Los filtros de URL →
useSearchParams - Resultado: -1.300 líneas más
Total
Antes: 3.100 líneas de Redux
Después: 180 líneas (Zustand store + custom hooks de TanStack Query)
Reducción: 94% menos código de gestión de estado
Y la app funciona igual. Mejor, de hecho, porque TanStack Query gestiona la caché mucho mejor que mi implementación manual con Redux.
¿Cuándo SÍ tiene sentido Redux?
No voy a decir “Redux nunca”. Hay casos donde sigue siendo la mejor opción:
- Editores visuales (tipo Figma, Photoshop web): Undo/redo nativo con Redux + Immer
- Apps offline-first: Redux Persist + middleware de sincronización
- Estado compartido entre muchos componentes con lógica compleja: Middleware, sagas, epics
- Equipos grandes que necesitan una arquitectura predecible y estandarizada
Si estás en alguno de estos casos, Redux Toolkit sigue siendo sólido. Para todo lo demás, hay alternativas más ligeras.
Resumen: Mi stack de estado en 2026
| Necesidad | Solución | Líneas de setup |
|---|---|---|
| Datos de API | TanStack Query | 5 por query |
| Estado global | Zustand | 20-30 (un store) |
| Formularios | React Hook Form | 0 (por form) |
| Estado de URL | nuqs | 3 por param |
| Estado local | useState/useReducer | 1-5 |
| Total | ~60 líneas |
Vs las 3.100 líneas que tenía con Redux. No hay comparación.
Artículos relacionados
- Cómo estructuro mis carpetas en React/Next.js — organización sin Redux
- Caso real: SaaS con NestJS y React — un SaaS sin Redux
- Testear código generado por IA — testing de hooks de estado