Redux

Tareas Pendientes

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:

  1. Store único
    • Una única fuente de la verdad que contiene el estado global.
    • Estado inmutable: sólo puede cambiarse mediante actions procesadas por reducers.
  2. Actions
    • Objetos simples { type: string, payload?: any }.
    • Describen qué ocurrió, no cómo cambia el estado.
  3. Reducers
    • Funciones puras (state, action) => newState.
    • Nunca mutan el estado directamente.
    • Permiten mantener trazabilidad y reproducibilidad.

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

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.: createSelector de Reselect).
    • Selectores compuestos para cálculos pesados.

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) => newState que 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 estados pending / 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 generators function*.
  • 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) → newState funcione 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