Redux is a library that keeps an app's state, the data the screen is drawn from, in one place called the store. You can read the state whenever you like, but you can't change it directly. To change it, you dispatch an action: a plain object that describes what happened, with a type saying what kind of thing it was, such as { type: "added", amount: 5 }. The store hands its current state and the action to a function called the reducer, keeps whatever the reducer returns as the new state, and then tells everyone who subscribed. A Redux-style store is any store that works this way, through three methods:
| Method | What it does |
|---|---|
store.getState() | Returns the current state. |
store.dispatch(action) | Calls the reducer with the current state and action, keeps the result as the new state, then calls every subscriber. It returns action. |
store.subscribe(listener) | Adds a function to call after every dispatch, and returns a function that removes it. Subscribers get no arguments: they call getState() to see what's new. |
A reducer is a pure function, (state, action) => nextState. Pure means its result depends only on its arguments, and it has no side effects: no requests, no timers, no random numbers, and no changes to the objects it was given. It looks at the action's type, works out what the state should be afterwards, and returns that. The name comes from Array.prototype.reduce: a reducer is the kind of function you'd pass to reduce, turning a list of actions into one state.
const counter = (state = 0, action) => {
if (action.type === "incremented") return state + 1;
if (action.type === "added") return state + action.amount;
return state;
};
counter(undefined, { type: "start" }); // 0: the default parameter fills in
counter(0, { type: "incremented" }); // 1
counter(1, { type: "added", amount: 5 }); // 6
counter(6, { type: "todoAdded" }); // 6: not its business, so the state comes back as it was
const actions = [{ type: "incremented" }, { type: "added", amount: 5 }];
actions.reduce(counter, undefined); // 6, as if a new store had been sent both actions
Why state = 0? A new store has no state yet, so before any action of yours it calls the reducer once, with state set to undefined and an action no reducer handles. The default parameter turns that undefined into the starting state, so each reducer declares its own starting value, right next to the code that changes it. If you give the store a starting state, the reducer gets that instead and the default isn't used. Defaults only replace undefined: null is a real state, and a reducer given null keeps it. Here's a store made with counter, step by step:
| Step | The reducer gets | The state is now | Subscribers |
|---|---|---|---|
| The store is created | undefined and the store's own first action | 0, the default | None yet: nobody can subscribe before the store exists. |
dispatch({ type: "incremented" }) | 0 and that action | 1 | All called; getState() gives 1. |
dispatch({ type: "added", amount: 5 }) | 1 and that action | 6 | All called; getState() gives 6. |
dispatch({ type: "todoAdded" }) | 6 and that action | 6, the same value | All called, and they can tell that nothing changed. |
Two rules keep this working. A reducer never changes state in place: to add a todo, it returns a new array, [...state, todo], instead of calling state.push(todo). And when an action doesn't concern it, it returns the state it was given, the very same object. Code that watches a store often decides whether to redraw by comparing old and new values with ===, as React Redux's useSelector does by default. A different object means something changed; the same object means nothing did. An object changed in place is still === to itself, so the change goes unnoticed, and a copy made when nothing changed makes everyone redraw for nothing.
In a real app the state is usually an object with one key per feature, such as { todos, filter }, and each key has its own small reducer that only ever sees its part. Redux's combineReducers joins them into the one reducer a store takes. You meet reducers in Redux and Redux Toolkit, in NgRx for Angular, and in React's useReducer hook, which keeps one component's state with a reducer and gives it a dispatch of its own. Redux Toolkit's createSlice lets you write reducers that look as if they change state; it uses the Immer library to turn those changes into a new object.
Currying turns a function that takes several arguments, such as add3(a, b, c), into one you call one argument at a time: add3(a)(b)(c). Each call takes an argument and returns a new function waiting for the next. The call that brings the last argument runs the original function and returns its result. It's named after the logician Haskell Curry.
// A curried add, written by hand: each call returns a function waiting for the next argument
const add = (a) => (b) => a + b;
add(2)(3); // 5
const add5 = add(5); // a function waiting for b, with a fixed at 5
add5(1); // 6
add5(10); // 15: add5 can be reused, and nothing changes it
[1, 2, 3].map(add5); // [6, 7, 8]
That's what makes currying handy: a half-applied function such as add5 is a new, more specific function you can store, reuse, and pass to map, filter, or an event listener. You fix the argument that stays the same once, and supply the one that varies later.
Writing every function this way is tedious, so helpers such as Lodash's _.curry curry an ordinary function for you. A function's length property is the number of parameters it declares before the first one that has a default value or starts with ...: 3 for (a, b, c) => a + b + c, but 0 for (...nums) => 0. The curried version collects arguments until it has that many, then calls the original. Lodash's version also accepts several arguments in one call. With const add3 = _.curry((a, b, c) => a + b + c):
| Call | Arguments so far | Returns |
|---|---|---|
add3(1) | 1 | A function waiting for more |
add3(1)(2) | 1, 2 | A function waiting for more |
add3(1)(2)(3) | 1, 2, 3 | 6: it now has as many arguments as the original expects, so it calls it |
add3(1, 2)(3) | 1, 2, 3 | 6 |
add3(1, 2, 3) | 1, 2, 3 | 6 |
Two variants are worth knowing. Lodash's _.curry also accepts a placeholder, _, that holds a position open for a later call: with const subtract = _.curry((a, b) => a - b), subtract(_, 1) fixes b and waits for a, so subtract(_, 1)(10) is 9. And when a function can't know how many arguments are coming, such as a sum that adds up whatever it's given, the caller ends the chain with an empty call: sum(1)(2)(3)() returns 6.
Currying is close to partial application, which fixes some arguments in one step and returns a function that takes all the rest in its next call. A curried function keeps returning functions until it has every argument, so you can fill them in a piece at a time.
Middleware is a list of functions that a request, or an action, passes through on its way to being handled. Each function, called a middleware, does one job, such as logging, checking who's signed in, or catching errors, then hands over to the next one. Web servers built with Express or Koa handle every HTTP request this way, and Redux sends every dispatched action through middleware before the reducer sees it. To add a feature, you add a function to the list, without changing the others.
In Koa, a middleware is a function (ctx, next). ctx, the context, is one object shared by every middleware for the same request: it holds the request, the response being built, and anything a middleware adds, such as ctx.user. next is a function that runs the rest of the list. A middleware can do some work, call next() to hand over, and once the rest has finished, do more work. That gives the onion picture: the request travels in through each layer in list order to the centre, then back out through the same layers in reverse. A middleware that doesn't call next() stops it there, and nothing further in runs.
// Logs how long the rest of the list took
const timer = async (ctx, next) => {
const start = Date.now();
await next();
console.log(`${ctx.path}: ${Date.now() - start} ms`);
};
// Adds the user to the context, or stops here with 401 Unauthorized
const auth = async (ctx, next) => {
ctx.user = await findUser(ctx.get("Authorization"));
if (!ctx.user) {
ctx.status = 401;
return; // no next(): the handler below never runs
}
await next();
};
const hello = (ctx) => {
ctx.body = `Hello, ${ctx.user.name}`;
};
// In Koa, app.use(timer), app.use(auth), and app.use(hello) add them in that order
| Step | A signed-in request | A request with no user |
|---|---|---|
| 1 | timer notes the start time and calls next(). | The same. |
| 2 | auth finds the user, sets ctx.user, and calls next(). | auth finds no user, sets the status to 401, and returns without calling next(). |
| 3 | hello sets the response body. It's the last, so it has no next() to call. | hello never runs. |
| 4 | auth's await next() finishes, and it has nothing more to do. | Nothing: auth has already finished. |
| 5 | timer's await next() finishes, and it logs the time. | The same: timer still logs the time. |
In Koa, next() returns a promise that settles once the rest of the list has finished. So a middleware can wait for it, time it, or wrap await next() in try...catch to turn an error from anywhere further in into a 500 response. Express middleware has the same idea in a different shape, (req, res, next), but its next() returns nothing to wait for. Work that should happen after the response, such as logging how long it took, is usually done in a listener for the response's finish event instead.
Redux middleware sits between dispatch and the reducer: every action you dispatch passes through it before the reducer sees it. It's used for logging actions, reporting crashes along with the action that caused them, and async work. Redux Thunk, which Redux Toolkit's configureStore adds by default, lets you dispatch a function instead of an action: the middleware calls that function with dispatch and getState, so it can fetch data and dispatch real actions when the response arrives.
A Redux middleware is a function in three layers: ({ getState, dispatch }) => (next) => (action) => result. The first layer gets the store's API, once, when the store is created. The second gets next, also once: the function that passes an action on to the next middleware, or, after the last one, to the store's own dispatch, which runs the reducer. The third layer runs for every action, and what it returns goes back to whoever dispatched the action or called its next.
// Logs every action, and the state once the reducer has run
const logger = ({ getState }) => (next) => (action) => {
console.log("action", action.type);
const result = next(action); // the rest of the chain, then the reducer
console.log("state", getState());
return result;
};
// Reports an error thrown further in, with the action that caused it
const crashReporter = () => (next) => (action) => {
try {
return next(action);
} catch (error) {
console.error("crashed on", action.type, error.message);
throw error;
}
};
// Dispatching { type: "incremented" } to a counter at 0, through both,
// logs "action incremented", then "state 1", and returns the action
| In the third layer | What happens |
|---|---|
return next(action) | Passes the action on, and returns what the rest of the chain returns. At the end of the chain, the store's dispatch returns the action. |
next({ ...action, at: Date.now() }) | Passes on a changed action: the middleware after this one, and the reducer, see the new one. |
Not calling next at all | Stops the action: the middleware after this one and the reducer never see it. Whatever this one returns goes back up the chain instead. |
dispatch(otherAction) | Starts otherAction from the first middleware, so it goes through all of them again. That's how the actions a thunk dispatches get logged too. |
Redux middleware sits between dispatch and the reducer. It can log actions, change them, stop them, or dispatch other actions, which is how Redux handles async work. Write applyMiddleware(...middlewares), the function Redux uses to add middleware to a store.
A middleware is a function in three layers: ({ getState, dispatch }) => (next) => (action) => result. The first layer gets the store's API. The second gets next, a function that passes an action on to the next middleware. The third handles each action, and what it returns is what dispatch returns.
applyMiddleware(...middlewares) returns an enhancer: a function that takes createStore and returns a new createStore. Calling that with (reducer, initialState) creates a store with the given createStore, then returns a copy of that store whose dispatch goes through the middleware. Everything else on the store, like getState and subscribe, stays the same.
createStore is any function like the one from the Redux-style store problem: createStore(reducer, initialState) returns an object with getState and dispatch, and maybe more methods.
An action dispatched to the new store goes through the middleware in the order they're listed. The next of the last middleware is the original store's dispatch, which runs the reducer. A middleware that doesn't call next stops the action there.
The dispatch that middleware get in their first layer is the new store's dispatch, so an action a middleware dispatches goes through all the middleware again, from the first. getState returns the store's current state.
The first two layers of each middleware run once, when the store is created; only the third runs for each action. A middleware that calls dispatch in its first layer, while the store is still being set up, gets an Error with the message "Dispatching while constructing your middleware is not allowed".
const logger = ({ getState }) => (next) => (action) => {
console.log("before", getState());
const result = next(action);
console.log("after", getState());
return result;
};
const thunk = ({ dispatch, getState }) => (next) => (action) =>
typeof action === "function" ? action(dispatch, getState) : next(action);
const counter = (state = 0, action) => (action.type === "inc" ? state + 1 : state);
const store = applyMiddleware(thunk, logger)(createStore)(counter, 0);
store.dispatch({ type: "inc" }); // logs "before 0", then "after 1"
store.dispatch((dispatch) => {
dispatch({ type: "inc" }); // logs "before 1", then "after 2"
return "done";
}); // returns "done"
store.getState(); // 2
const createStore = (reducer, state) => ({
getState: () => state,
dispatch(action) {
state = reducer(state, action);
return action;
},
});
const counter = (state, action) => (action.type === "inc" ? state + 1 : state);
const store = applyMiddleware()(createStore)(counter, 5);
const action = { type: "inc" };
expect(store.dispatch(action)).toBe(action);
expect(store.getState()).toBe(6);
const subscribe = spy();
const createStore = spy((reducer, state) => ({ getState: () => state, dispatch: (action) => action, subscribe }));
const reducer = (state) => state;
const store = applyMiddleware()(createStore)(reducer, 1);
expect(createStore).toHaveBeenCalledTimes(1);
expect(createStore).toHaveBeenCalledWith(reducer, 1);
expect(store.getState()).toBe(1);
store.subscribe("listener");
expect(subscribe).toHaveBeenCalledWith("listener");
const createStore = (reducer, state) => ({
getState: () => state,
dispatch(action) {
state = reducer(state, action);
return action;
},
});
const log = [];
const tag = (name) => () => (next) => (action) => {
log.push(name + " in");
const result = next(action);
log.push(name + " out");
return result;
};
const reducer = (state) => {
log.push("reducer");
return state;
};
const store = applyMiddleware(tag("a"), tag("b"), tag("c"))(createStore)(reducer, 0);
store.dispatch({ type: "x" });
expect(log).toEqual(["a in", "b in", "c in", "reducer", "c out", "b out", "a out"]);
const createStore = (reducer, state) => ({
getState: () => state,
dispatch(action) {
state = reducer(state, action);
return action;
},
});
const double = () => (next) => (action) => next({ ...action, by: action.by * 2 });
const reducer = (state, action) => (action.type === "add" ? state + action.by : state);
const store = applyMiddleware(double, double)(createStore)(reducer, 0);
store.dispatch({ type: "add", by: 1 });
expect(store.getState()).toBe(4);
const createStore = (reducer, state) => ({
getState: () => state,
dispatch(action) {
state = reducer(state, action);
return action;
},
});
const block = () => (next) => (action) => (action.type === "blocked" ? "stopped" : next(action));
const reducer = spy((state, action) => (action.type === "inc" ? state + 1 : state));
const store = applyMiddleware(block)(createStore)(reducer, 0);
expect(store.dispatch({ type: "blocked" })).toBe("stopped");
expect(reducer).not.toHaveBeenCalled();
store.dispatch({ type: "inc" });
expect(store.getState()).toBe(1);
const createStore = (reducer, state) => ({
getState: () => state,
dispatch(action) {
state = reducer(state, action);
return action;
},
});
const seen = [];
const logger = () => (next) => (action) => {
seen.push(typeof action === "function" ? "thunk" : action.type);
return next(action);
};
const thunk = ({ dispatch, getState }) => (next) => (action) =>
typeof action === "function" ? action(dispatch, getState) : next(action);
const counter = (state, action) => (action.type === "inc" ? state + 1 : state);
const store = applyMiddleware(logger, thunk)(createStore)(counter, 0);
const result = store.dispatch((dispatch, getState) => {
dispatch({ type: "inc" });
dispatch({ type: "inc" });
return getState();
});
expect(result).toBe(2);
expect(seen).toEqual(["thunk", "inc", "inc"]);
const createStore = (reducer, state) => ({
getState: () => state,
dispatch(action) {
state = reducer(state, action);
return action;
},
});
const handle = spy((next) => (action) => next(action));
const setup = spy(() => handle);
const store = applyMiddleware(setup)(createStore)((state) => state, 0);
store.dispatch({ type: "a" });
store.dispatch({ type: "b" });
expect(setup).toHaveBeenCalledTimes(1);
expect(handle).toHaveBeenCalledTimes(1);
expect(typeof setup.calls[0].args[0].getState).toBe("function");
expect(typeof setup.calls[0].args[0].dispatch).toBe("function");
const createStore = (reducer, state) => ({
getState: () => state,
dispatch(action) {
state = reducer(state, action);
return action;
},
});
const eager = ({ dispatch }) => {
dispatch({ type: "too soon" });
return (next) => (action) => next(action);
};
expect(() => applyMiddleware(eager)(createStore)((state) => state, 0)).toThrow(
"Dispatching while constructing your middleware is not allowed",
);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Redux applyMiddleware. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.