Frontend
Uso con
Optimistic update usando
Ejemplo con
Redux
- Frontend
- gestion de estado
- context api
- docs
- Comparativas:
- redux vs akita
- redux vs context
- alternativas
- zustand
Tareas Pendientes
- Redux con Next.js siguiendo la guía:
- Redux con Angular usando NgRx
- NgRx Store
- Uso de Redux
- Redux + Next.js
Fundamentos de Redux
Redux es un contenedor predecible de estado para aplicaciones JavaScript, basado en un flujo unidireccional y en la inmutabilidad del estado.
Redux trabaja sobre tres principios fundamentales:
- Store único
- Una única fuente de la verdad que contiene el estado global.
- Estado inmutable: sólo puede cambiarse mediante actions procesadas por reducers.
- Actions
- Objetos simples
{ type: string, payload?: any }. - Describen qué ocurrió, no cómo cambia el estado.
- Objetos simples
- Reducers
- Funciones puras
(state, action) => newState. - Nunca mutan el estado directamente.
- Permiten mantener trazabilidad y reproducibilidad.
- Funciones puras
Flujo Unidireccional
- Los componentes disparan actions.
- Las actions viajan al store.
- El store ejecuta los reducers.
- Los reducers generan un nuevo estado.
- Los componentes reciben la actualización mediante suscripción.
Manejo Avanzado de Estado
Middleware
- Añaden capacidades entre el dispatch y el reducer.
- Ejemplos: redux-thunk, redux-saga, redux-observable.
- Permiten:
- Control de flujos asíncronos
- Side-effects controlados
- Logging, métricas, seguridad y auditoría
Normalización de estado
- Evita estructuras anidadas difíciles de mantener.
- Se recomienda usar patrones tipo entity store o herramientas como createEntityAdapter (Redux Toolkit).
Rehidratación / Persistencia
- Permite restaurar estado desde localStorage o APIs.
- Complementos comunes: redux-persist.
Redux DevTools
- Extensión para inspeccionar el store en tiempo real.
- Permite:
- Ver actions disparadas
- Ver la evolución del estado
- Time-travel debugging
- Exportar y reproducir sesiones
Redux Toolkit (RTK)
- API oficial recomendada.
- Simplifica:
- Creación de reducers con createSlice
- Configuración del store con configureStore
- Lógica asíncrona con createAsyncThunk
- Inmutabilidad automática vía Immer
- Reduce ceremonia y errores comunes.
- Integra buenas prácticas por defecto.
Conceptos clave de Redux Toolkit
- Slices: estado + reducers + actions en un mismo módulo.
- Thunks: funciones async que envían actions antes/después.
- RTK Query: sistema integrado de fetch + caching para APIs.
Integraciones y Ecosistema
React
- Biblioteca estándar: react-redux
- Hooks modernos:
useSelector()useDispatch()useStore()
- Recursos:
Angular
- Implementación recomendada: NgRx
- Basado en Redux pero adaptado a RxJS:
- Stores reactivos
- Effects (similar a sagas)
- Reducers + actions al estilo Redux
- Recurso:
Comparación: Redux vs Context API
- Context es adecuado para:
- Estado simple o de alcance local
- Theming, user session, pequeños flags
- Redux es preferible cuando:
- Hay múltiples flujos de estado complejos
- Necesitas herramientas avanzadas (DevTools)
- Control estricto del ciclo de estado
- Sincronización con APIs complejas
- Recurso comparativo:
Redux vs React Context
Redux — Conceptos Avanzados y Arquitecturas Modernas (2025)
Arquitecturas basadas en Redux
Feature-Based Architecture
- Organización del código por funcionalidades en lugar de tipos (reducers, actions).
- Cada feature contiene:
- Su slice (RTK)
- Selectores
- Componentes asociados
- Side-effects
- Ventajas:
- Escalabilidad
- Fácil mantenimiento
- Menos coupling entre módulos
Domain-Driven Redux
- Enfoque basado en el dominio, alineando Redux con los bounded contexts definidos en DDD.
- Cada dominio tiene:
- Store propio (o slice)
- Modelos de estado normalizados
- Acciones específicas del dominio
- Útil para sistemas complejos (ERP, marketplaces, apps financieras).
State Modeling en Redux
Estado derivado (Derived State)
- Datos calculados a partir del estado primario usando selectores.
- Evita duplicaciones innecesarias.
- Se recomienda usar:
- Selectores memoizados (ej.:
createSelectorde Reselect). - Selectores compuestos para cálculos pesados.
- Selectores memoizados (ej.:
Estado local vs Global
- Criterios para decidir si algo merece estar en Redux:
- ¿Se comparte entre multiple componentes?
- ¿Necesita persistencia?
- ¿Requiere trazabilidad?
- ¿Es necesario debugear su evolución?
Selectores Avanzados
- Memoización profunda para evitar renders innecesarios.
- Composición de selectores para reducir lógica en componentes.
- Patrones:
- Selector factories: selectores que aceptan parámetros.
- Encapsular selectores por feature.
Patrones de Side Effects
Redux Thunk Avanzado
- Thunks secuenciales y dependientes.
- Encadenamiento de side-effects.
- Cancelación manual mediante señales.
Redux Saga
- Efectos asíncronos declarativos:
takeLatest,takeEvery,debounce,throttle
- Adecuado para flujos complejos: pagos, transacciones, sockets.
Redux Observable (RxJS)
- Para Angular o apps orientadas a streams.
- Manejo superior de flujos event-driven.
Testing en Redux
Reducers
- Pruebas puras: input del state anterior + action → output esperado.
Selectores
- Pruebas con estados simulados.
- Garantizar memoización.
Middleware y lógica asíncrona
- Mock de APIs
- Fake timers
- Testing aislado del store
RTK Query Testing
- Mock de endpoints
- Validación del cache y estados:
pending → fulfilled → error
Patrones de Optimización
Reducir Re-renders
- Uso adecuado de
useSelector+ memoización. - Evitar selectores que devuelvan nuevas referencias.
- Slices pequeños → menor área de notificación.
State Partitioning
- División del estado global en dominios más pequeños.
- Permite lazy-loading de slices.
Lazy Loading de Reducers
- Reducers que se inyectan dinámicamente.
- Ideal para aplicaciones con microfrontends o rutas cargadas bajo demanda.
Redux en Aplicaciones Escalables (2025)
Microfrontends
- Stores aislados por microfrontend.
- Sincronización mediante eventos o message bus.
- Adopción creciente de:
- Module Federation + Redux
- Web Components + stores locales sincronizados
SSR y Streaming con Next.js
- Hidratar el store en servidor y enviarlo al cliente.
- RTK Query compatible con:
- SSR
- ISR
- Rutas híbridas
Offline First
- Integración con service workers.
- Persistencia del cache de RTK Query.
- Manejo de colas de acciones offline.
Cuándo No Usar Redux
- Estado estrictamente local.
- Funciones UI puras que no necesitan sincronización.
- Apps pequeñas sin side-effects complejos.
- Casos donde signals (Preact, Angular, Solid.js) ofrecen mejor granularidad reactiva.
Nuevos Temas para Explorar
- Redux con WebSockets vía Sagas u Observables.
- Patrones para sincronizar Redux con:
- IndexedDB
- BroadcastChannel
- Migración a Redux Toolkit desde Redux clásico.
- Diseño de stores para apps con IA embebida (contextos dinámicos).
- Aplicación de Zod/TypeScript para validar payloads globales.
Glosario de Redux (2025)
Conceptos Fundamentales
- Store
Contenedor único del estado global de la aplicación. Inmutable, solo se modifica mediante acciones procesadas por reducers. - State
Árbol de datos que representa el estado actual de la aplicación. - Action
Objeto plano que describe un evento:{ type, payload }. - Reducer
Función pura(state, action) => newStateque devuelve un nuevo estado sin mutar el anterior. - Dispatch
Mecanismo para enviar acciones al store. - Flujo Unidireccional
Patrón donde los datos siempre fluyen: View → Action → Reducer → New State → View.
Redux Toolkit (RTK)
- createSlice
Crea reducers + actions asociados en un mismo módulo. - configureStore
Utilidad para crear un store con middleware y devtools preconfigurados. - createAsyncThunk
Genera thunks para lógica asíncrona con estadospending / fulfilled / rejected. - Immer
Librería incluida en RTK que permite escribir “código mutable” produciendo estados inmutables. - RTK Query
Sistema integrado para data fetching y caching con endpoints declarativos.
Selectores
- Selector
Función que obtiene una parte del estado:(state) => state.user. - Memoización
Técnica para evitar recalcular valores derivados si el estado no cambió. - createSelector (Reselect)
Crea selectores memoizados eficientes. - Selector Factory
Selector que recibe parámetros:(id) => createSelector(...).
Middleware
- Middleware
Funciones que interceptan acciones antes de llegar al reducer. - redux-thunk
Permite actions asíncronas basadas en funciones. - redux-saga
Manejo de side-effects declarativos mediante generatorsfunction*. - redux-observable
Side-effects basados en RxJS (streams). - Logger Middleware
Registra cada action y estado resultante.
Manejo Avanzado del Estado
- Estado Normalizado
Estructura plana donde entidades se guardan por id. Facilita updates y evita duplicación. - Entidad
Unidad de datos con un identificador único (ej. usuario, producto). - createEntityAdapter
Herramienta RTK para manejar colecciones normalizadas. - Derived State
Estado que se calcula desde otros datos, no se guarda en el store. - Rehidratación
Proceso de restaurar el estado desde almacenamiento persistente.
Integración con Frameworks
React
- react-redux
Biblioteca oficial para conectar componentes con el store. - Provider
Componente que expone el store al árbol de componentes. - useSelector
Hook para leer estado desde el store. - useDispatch
Hook para despachar acciones. - useStore
Acceso directo al store (poco habitual).
Angular con NgRx
- Actions
Igual concepto que Redux clásico pero con typings estrictos. - Reducers
Mismo principio: funciones puras. - Effects
Side-effects basados en RxJS streams. - Selectors Memoizados
Fundamentales por naturaleza de Angular.
Arquitectura y Organización
- Feature-based Structure
Organización del código por funcionalidades. - Domain-driven Redux
Store estructurado por dominios del negocio. - State Partitioning
División del estado en slices independientes. - Lazy Reducers
Reducers cargados dinámicamente (útil en microfrontends). - Bounded Context
Sub-dominios autónomos dentro del store global.
Debug y Herramientas
- Redux DevTools
Extensión para inspeccionar acciones y estados. - Time Travel Debugging
Permite navegar entre acciones para reproducir estados previos. - Action Replay
Repetir acciones para reproducir un bug.
Testing
- Pruebas de Reducers
Validan que(state, action) → newStatefuncione correctamente. - Pruebas de Selectores
Comprueban salidas con diferentes estados; verifican memoización. - Mock de Store
Para probar componentes o lógica aislada. - Testing de Thunks
Mock de APIs y verificación de dispatches.
RTK Query
- Endpoint
Función declarativa que define cómo se obtiene o muta un recurso remoto. - Query
Fetch de datos con caching automático. - Mutation
Actualizar datos en el servidor. - Cache Tags
Etiquetas que controlan invalidaciones automáticas. - Auto-fetching
Refetch automático según reglas de invalidación.
Ecosistema y Casos Especiales
- SSR con Next.js
Hidratación del estado entre servidor y cliente. - Persistencia
Integración con localStorage/IndexedDB (ej: redux-persist). - BroadcastChannel
Sincronización de estado entre pestañas. - WebSockets + Redux
Side-effects con sagas u observables para tiempo real. - Feature Toggles
Flags de funcionalidades controlados desde Redux. - Offline First
Colas de acciones, reintentos y cache persistente.
Redux — Ejemplos de Código (2025)
Setup básico de Redux (sin Toolkit)
Store, Actions y Reducer
import { createStore } from "redux";
// Estado inicial
const initialState = {
count: 0
};
// Actions
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
// Reducer
function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
}
// Crear Store
const store = createStore(counterReducer);
// Usar el store
store.subscribe(() => console.log(store.getState()));
store.dispatch(increment());
store.dispatch(decrement());
`
Setup con Redux Toolkit (RTK)
createSlice + configureStore
import { configureStore, createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value++;
},
decrement: (state) => {
state.value--;
},
incrementBy: (state, action) => {
state.value += action.payload;
}
}
});
export const { increment, decrement, incrementBy } = counterSlice.actions;
export const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
Redux + React (React Redux)
Provider
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);
Uso con useSelector y useDispatch
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, incrementBy } from "./store";
export default function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>Valor actual: {count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(incrementBy(5))}>+5</button>
</div>
);
}
Thunks con Redux Toolkit
createAsyncThunk
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchUser = createAsyncThunk(
"user/fetch",
async (id, thunkAPI) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
return await res.json();
}
);
const userSlice = createSlice({
name: "user",
initialState: {
data: null,
status: "idle",
error: null
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = "loading";
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = "succeeded";
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
}
});
export default userSlice.reducer;
RTK Query — Ejemplo completo
API Service
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const usersApi = createApi({
reducerPath: "usersApi",
baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com" }),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => "/users"
}),
getUserById: builder.query({
query: (id) => `/users/${id}`
})
})
});
export const { useGetUsersQuery, useGetUserByIdQuery } = usersApi;
Integrar API en el Store
import { configureStore } from "@reduxjs/toolkit";
import { usersApi } from "./usersApi";
export const store = configureStore({
reducer: {
[usersApi.reducerPath]: usersApi.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(usersApi.middleware)
});
Uso en un componente
import { useGetUsersQuery } from "./usersApi";
export default function UsersList() {
const { data, isLoading, error } = useGetUsersQuery();
if (isLoading) return <p>Cargando...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Selectores Memoizados con Reselect
import { createSelector } from "reselect";
const selectProducts = (state) => state.products.list;
const selectFilter = (state) => state.products.filter;
export const selectFilteredProducts = createSelector(
[selectProducts, selectFilter],
(products, filter) => {
return products.filter((p) => p.category === filter);
}
);
Lazy Loading de Reducers
export function injectReducer(store, key, reducer) {
if (!store.asyncReducers) store.asyncReducers = {};
if (!store.asyncReducers[key]) {
store.asyncReducers[key] = reducer;
store.replaceReducer(
combineReducers({
...store.initialReducers,
...store.asyncReducers
})
);
}
}
Middleware personalizado
const loggerMiddleware = (store) => (next) => (action) => {
console.log("Dispatching:", action.type);
const result = next(action);
console.log("Next state:", store.getState());
return result;
};
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware)
});
Redux — Ejemplos Avanzados (2025)
WebSockets + Redux (con Redux Saga)
Configuración del canal WebSocket
import { eventChannel } from "redux-saga";
import { call, take, put, takeEvery } from "redux-saga/effects";
function createSocketChannel(socket) {
return eventChannel((emit) => {
socket.onmessage = (event) => {
emit(JSON.parse(event.data));
};
return () => socket.close();
});
}
function* listenWebSocket() {
const socket = new WebSocket("wss://example.com/events");
const channel = yield call(createSocketChannel, socket);
while (true) {
const payload = yield take(channel);
yield put({ type: "WS_MESSAGE_RECEIVED", payload });
}
}
export function* rootSaga() {
yield takeEvery("WS_CONNECT", listenWebSocket);
}
`
Microfrontends — Injectar reducers dinámicamente
Host cargando reducers remotos
export function injectRemoteReducer(store, sliceName, reducer) {
if (!store.asyncReducers) store.asyncReducers = {};
if (!store.asyncReducers[sliceName]) {
store.asyncReducers[sliceName] = reducer;
store.replaceReducer(
combineReducers({
...store.staticReducers,
...store.asyncReducers
})
);
}
}
// Ejemplo: microfrontend remoto expone un reducer
import remoteOrdersReducer from "remote/ordersSlice";
injectRemoteReducer(store, "orders", remoteOrdersReducer);
SSR con Next.js + Redux Toolkit
Store Hydration
// store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
export function makeStore(preloadedState = {}) {
return configureStore({
reducer: { counter: counterReducer },
preloadedState
});
}
Uso en getServerSideProps
// pages/index.js
import { makeStore } from "../store";
import { increment } from "../counterSlice";
export async function getServerSideProps() {
const store = makeStore();
store.dispatch(increment());
return {
props: {
initialReduxState: store.getState()
}
};
}
Hidratar en el cliente
// _app.js
import { Provider } from "react-redux";
import { makeStore } from "../store";
export default function App({ Component, pageProps }) {
const store = makeStore(pageProps.initialReduxState);
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
Redux + IndexedDB / Persistencia Offline
Guardar y cargar estado
export const persistMiddleware = (store) => (next) => async (action) => {
const result = next(action);
const state = store.getState();
await indexedDBInstance.save("state", state);
return result;
};
export async function loadInitialState() {
return (await indexedDBInstance.get("state")) || undefined;
}
Inicializar el store con estado persistido
const preloaded = await loadInitialState();
const store = configureStore({ reducer, preloadedState: preloaded });
RTK Query — Actualización optimista (Optimistic Updates)
Optimistic update usando onQueryStarted
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const todosApi = createApi({
reducerPath: "todosApi",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
updateTodo: builder.mutation({
query: (todo) => ({
url: `/todos/${todo.id}`,
method: "PUT",
body: todo
}),
async onQueryStarted(todo, { dispatch, queryFulfilled }) {
const patch = dispatch(
todosApi.util.updateQueryData("getTodos", undefined, (draft) => {
const item = draft.find((i) => i.id === todo.id);
Object.assign(item, todo);
})
);
try {
await queryFulfilled;
} catch {
patch.undo();
}
}
})
})
});
Redux Saga — Retry, Debounce y Flujo Complejo
Ejemplo con retry + debounce
import { debounce, retry, call, put } from "redux-saga/effects";
function* fetchSearch(action) {
try {
const data = yield retry(3, 1000, fetch, `/search?q=${action.payload}`);
yield put({ type: "SEARCH_SUCCESS", payload: data });
} catch (e) {
yield put({ type: "SEARCH_ERROR", error: e.message });
}
}
export function* searchSaga() {
yield debounce(400, "SEARCH_REQUEST", fetchSearch);
}
Patrones Event-Driven (Redux Observable)
Transformar streams con RxJS
import { ofType } from "redux-observable";
import { map, mergeMap, debounceTime } from "rxjs/operators";
import { ajax } from "rxjs/ajax";
export const searchEpic = (action$) =>
action$.pipe(
ofType("SEARCH_TERM_CHANGED"),
debounceTime(300),
mergeMap((action) =>
ajax.getJSON(`/search?q=${action.payload}`).pipe(
map((response) => ({ type: "SEARCH_SUCCESS", payload: response }))
)
)
);
Normalización de Datos (EntityAdapter)
Ejemplo completo
import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";
const productsAdapter = createEntityAdapter({
selectId: (product) => product.id,
sortComparer: (a, b) => a.name.localeCompare(b.name)
});
const productsSlice = createSlice({
name: "products",
initialState: productsAdapter.getInitialState(),
reducers: {
setProducts: productsAdapter.setAll,
addProduct: productsAdapter.addOne,
updateProduct: productsAdapter.updateOne,
removeProduct: productsAdapter.removeOne
}
});
export const productsSelectors = productsAdapter.getSelectors(
(state) => state.products
);
Redux Middleware Avanzado — Eventos Globales
BroadcastChannel para sincronizar pestañas
const bc = new BroadcastChannel("redux-sync");
export const syncMiddleware = (store) => (next) => (action) => {
const result = next(action);
bc.postMessage(action);
return result;
};
bc.onmessage = (ev) => {
store.dispatch(ev.data);
};
Middleware de Auditoría (traza de acciones)
const auditMiddleware = (store) => (next) => (action) => {
const start = performance.now();
const result = next(action);
const duration = performance.now() - start;
console.log(`[AUDIT] Action: ${action.type} (${duration.toFixed(2)}ms)`);
return result;
};
Automatización de Flujos con createListenerMiddleware
Escuchar acciones sin Sagas/Thunks
import { createListenerMiddleware } from "@reduxjs/toolkit";
const listener = createListenerMiddleware();
listener.startListening({
actionCreator: loginSuccess,
effect: async (action, listenerApi) => {
await listenerApi.delay(300);
listenerApi.dispatch(fetchUserData(action.payload.id));
}
});
const store = configureStore({
reducer,
middleware: (gdm) => gdm().prepend(listener.middleware)
});
Si quieres, puedo generarte otra nota con **patrones arquitectónicos avanzados**, **microfrontends + Redux Toolkit**, **SSR extremo**, o **RTK Query + WebSockets**.
Omnivore Redux
type: list
name: "Notas con #powershell en redux"
order:
- property: date_saved
direction: desc
columns:
- file.name
- date_saved
filters:
and:
- file.inFolder("Omnivore")
- file.hasTag("redux", "Redux")
views:
- type: table
name: Table
sort:
- property: file.mtime
direction: DESC
¿Te gusta este contenido? Suscríbete vía RSS