๐ช๏ธ Advanced Redux Patterns, Architecture & Complex Concepts
Redux is much more than a global state manager. At scale, it becomes a data-flow architecture with middleware pipelines, domain-based slices, complex selectors, and performance-centric designs. Let's dig deep.
๐ง Core Concepts Revisited
Redux operates on three principles:
- Single source of truth (Store)
- State is read-only (Actions)
- Changes via pure functions (Reducers)
But when applications grow:
- State shapes evolve
- Action payloads become dynamic
- Middleware chains increase
- Async logic becomes complex (debouncing, retries, batching)
๐ฆ Custom Redux Store from Scratch
import { createStore, applyMiddleware, compose } from "redux";
// Root reducer (pure function)
const rootReducer = (state = {}, action) => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, action.payload],
};
default:
return state;
}
};
// Custom logging middleware
const logger = (store) => (next) => (action) => {
console.log("Dispatching:", action);
const result = next(action);
console.log("Next State:", store.getState());
return result;
};
// Composed store with dev tools
const store = createStore(
rootReducer,
{ todos: [] },
compose(
applyMiddleware(logger),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);โ๏ธ Advanced Middleware Patterns
๐ Retry Middleware
const retryMiddleware =
({ dispatch }) =>
(next) =>
(action) => {
if (!action.meta?.retry) return next(action);
let attempts = 0;
const maxAttempts = action.meta.retry;
const attempt = () => {
attempts++;
const result = next(action);
if (result.error && attempts < maxAttempts) {
setTimeout(attempt, 1000);
}
};
attempt();
};Use Case: For flaky API endpoints.
๐งฉ Dynamic Module Injection (Micro-Frontend Friendly)
function injectReducer(store, key, reducer) {
store.asyncReducers[key] = reducer;
store.replaceReducer(createRootReducer(store.asyncReducers));
}Use Case: Dynamic pages loading their reducers on-demand.
๐งต Redux + Web Workers (Multithreading)
// worker.js
onmessage = (e) => {
const result = heavyComputation(e.data);
postMessage(result);
};
// In Redux middleware
const worker = new Worker("./worker.js");
const offloadMiddleware = (store) => (next) => (action) => {
if (action.type !== "HEAVY_TASK") return next(action);
worker.postMessage(action.payload);
worker.onmessage = (e) => {
store.dispatch({ type: "HEAVY_TASK_RESULT", payload: e.data });
};
};๐ฆ Redux Saga vs Thunks (Advanced Comparisons)
Saga
function* fetchUser() {
try {
const user = yield call(api.fetchUser);
yield put({ type: "USER_SUCCESS", payload: user });
} catch (e) {
yield put({ type: "USER_ERROR", payload: e });
}
}
function* rootSaga() {
yield takeLatest("USER_FETCH", fetchUser);
}Use Case:
- Better for handling concurrency, race conditions, debounce logic.
Comparison:
- Thunks are simpler, co-located logic.
- Sagas shine in orchestrating multiple effects.
๐ง Complex Selectors (Recomputing & Memoization)
import { createSelector } from "reselect";
const selectTodos = (state) => state.todos;
export const selectIncompleteTodos = createSelector([selectTodos], (todos) =>
todos.filter((todo) => !todo.completed)
);Optimization Use Case:
- Avoid recomputation unless
todosarray reference changes.
๐งฑ Feature-Based Redux Folder Structure
src/
โโโ features/
โโโ users/
โโโ UserSlice.js
โโโ UserSaga.js
โโโ UserSelectors.js
โโโ UserService.jsBenefits: Isolation, scalability, dynamic injection.
๐ฅ Real-world Case: Redux + WebSocket
const socketMiddleware = (store) => (next) => (action) => {
if (action.type === "WS_CONNECT") {
const socket = new WebSocket(action.payload.url);
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
store.dispatch({ type: "WS_MESSAGE", payload: message });
};
store.dispatch({ type: "WS_CONNECTED" });
}
return next(action);
};๐งญ Final Thoughts
Redux can handle massive, event-driven systems when:
- State is normalized
- Async logic is organized via sagas/thunks
- Middlewares are lean and predictable
- Feature-based slices are lazy-loaded