Redux middleware is a wrapper around dispatch: it sees every action on the way to the reducer and can log it, transform it, delay it, or swallow it. Middleware are written with a curried signature — store => next => action => result — and composed into a chain, so each one hands the action to the next one and the last hands it to the store's real dispatch. Here you build a small Redux core (createStore and applyMiddleware) plus two classic middleware: thunk, which lets you dispatch a function of (dispatch, getState), and a logger that records the state around every action. If you have used Redux, this is the machinery behind applyMiddleware and redux-thunk.
type Action = { type: string };
type Dispatch = (action: Action | Function) => unknown;
type StoreAPI = { getState: () => unknown; dispatch: Dispatch };
// Every middleware is curried: store => next => action => result
type Middleware = (store: StoreAPI) => (next: Dispatch) => (action: unknown) => unknown;
const reduxMiddlewareThunkLogger: {
// Minimal Redux core.
createStore(reducer: Function, preloadedState?: unknown, enhancer?: Function): {
getState(): unknown;
dispatch: Dispatch;
subscribe(listener: () => void): () => void; // returns an unsubscribe fn
};
// Turns a list of middleware into a store enhancer.
applyMiddleware(...middlewares: Middleware[]): Function;
// A bare middleware: dispatch a function to run it as a thunk.
thunk: Middleware;
// A middleware factory: pass a sink to receive each log entry.
logger(sink?: (entry: { action: unknown; prevState: unknown; nextState: unknown }) => void): Middleware;
};
const { createStore, applyMiddleware, logger } = reduxMiddlewareThunkLogger;
const reducer = (s = { count: 0 }, a) => (a.type === 'INC' ? { count: s.count + 1 } : s);
const log = [];
const store = createStore(reducer, applyMiddleware(logger((e) => log.push(e))));
store.dispatch({ type: 'INC' });
// store.getState() → { count: 1 }
// log[0] → { action: { type: 'INC' }, prevState: { count: 0 }, nextState: { count: 1 } }
const { createStore, applyMiddleware, thunk } = reduxMiddlewareThunkLogger;
const reducer = (s = { count: 0 }, a) => (a.type === 'INC' ? { count: s.count + 1 } : s);
const store = createStore(reducer, applyMiddleware(thunk));
const bumpTwice = (dispatch) => {
dispatch({ type: 'INC' });
dispatch({ type: 'INC' });
};
store.dispatch(bumpTwice);
// store.getState() → { count: 2 } — the thunk dispatched two plain actions
store => next => action. The outer layers run once (store setup, then chain wiring); only the innermost action => result runs on every dispatch.thunk directly, but call logger(sink) first to configure the sink and get a middleware back.dispatch(plainAction), that action flows through the whole middleware chain again, so the logger records it too.logger() with no argument must not throw.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.