Redux is a state container: one plain object holds the whole app state, the only way to change it is to dispatch an action through a pure reducer, and any number of listeners can subscribe to be told when it changes. You will build that core plus the two helpers every real Redux app leans on, combineReducers and applyMiddleware, exposed as one object miniRedux.
createStore(reducer, preloadedState?, enhancer?) returns a store. dispatch(action) computes the next state with reducer(state, action) and then synchronously runs every subscriber. subscribe(listener) returns a function that unsubscribes. combineReducers(map) splices many small reducers into one. applyMiddleware(...mws) is an enhancer that wraps dispatch in a chain of middleware.
type Action = { type: string; [key: string]: unknown };
type Reducer<S> = (state: S | undefined, action: Action) => S;
type Middleware = (api: { getState(): unknown; dispatch(a: Action): Action }) =>
(next: (a: Action) => Action) => (action: Action) => Action;
const miniRedux: {
createStore<S>(reducer: Reducer<S>, preloadedState?: S, enhancer?: Function): {
getState(): S;
dispatch(action: Action): Action;
subscribe(listener: () => void): () => void; // returns unsubscribe
};
combineReducers(map: Record<string, Reducer<unknown>>): Reducer<Record<string, unknown>>;
applyMiddleware(...mws: Middleware[]): Function; // an enhancer for createStore
};
const { createStore } = miniRedux;
const counter = (state = 0, action) =>
action.type === 'INC' ? state + 1 : state;
const store = createStore(counter);
store.getState(); // 0 — the reducer's initial state
const unsubscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'INC' }); // logs 1
store.dispatch({ type: 'INC' }); // logs 2
unsubscribe();
store.dispatch({ type: 'INC' }); // state is 3, but nothing logs
const { createStore, combineReducers, applyMiddleware } = miniRedux;
const logger = (store) => (next) => (action) => {
const result = next(action);
console.log(action.type, store.getState());
return result;
};
const root = combineReducers({ count: counter });
const store = createStore(root, applyMiddleware(logger));
store.dispatch({ type: 'INC' }); // logs: INC { count: 1 }
store.getState(); // { count: 1 }
dispatch runs the reducer and notifies every subscriber before it returns. There are no timers and nothing is async.combineReducers returns the exact same state object when no slice changed, so reference-equality consumers can skip work.store => next => action => {}, composed right-to-left, and each one receives { getState, dispatch } where dispatch routes through the whole chain.replaceReducer, time-travel, or React bindings. The three functions above are the whole surface.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.