It’s not searching for anything, you are calling the reducer
function (that you have written) with a parameter. That’s why I say it doesn’t “search” anywhere, because you are literally just calling a [simple] function with a very specific parameter.
const loginAction = { type: "LOGIN" }
reducer(state, action) {
if (action.type === "LOGIN") {
// do thing
// ...later, when `dispatch` gets called, which passes the
// `loginAction` to your `reducer` function, it runs:
reducer(the_current_state, {type: "LOGIN"})
So say you have an application which lets people write articles. And admins can log in, review the articles, and publish them. Your admin has logged in, and can see the articles they need to review, and maybe they have some notifications as well. The state of the app (what it looks like at a specific moment in time given a specific set of user actions) is kinda like
- a user has logged in
- their own personal details have been loaded
- they have an authorisation level of level of “admin”
- they have access to a list of posts wating for review
- they have some unread notifications
You want to keep track of the state of an app (because it’s the most important thing about any given app). You could do it on an ad-hoc basis: when stuff happens, do other stuff. But this is error-prone and gets messy very very quickly. So one approach is to put all the important state in one place. You could model the state in JS using an object:
const state = {
auth: {
isLoggedIn: true,
},
user: {
authLevel: "admin",
name: "Amy the Admin",
email: "amytheadmin@example.com",
},
postsToReview:[
"696e3f62-57ca-11ea-82b4-0242ac130003",
"696e4174-57ca-11ea-82b4-0242ac130003",
],
notifications: [
{ id: "bd5b65dc-57ca-11ea-8e2d-0242ac130003", type: "FLAGGED_POST" },
],
}
Now, you can make that available all through your app. Like, literally just have the state object with all possible default values as the first thing in your code, then have the rest of your code read from it and modify it. This is a bit difficult though:
- you need to set up getters and setters for everything
- what happens when a value updates? How do you make sure the UI understands it
- how do you stop multiple things modifying it at the same time, or accidentally deleting bits of the state?
- how do you debug it?
- etc etc
So what Redux does is provide a safe interface for this. You have that object that represents your state at any one point in time. But you cannot modify the state directly. It is stored in another object (the “store”), and the only way to change the state is by sending messages (“actions”) to that store. You define what those messages are, and you also define a set of functions (reducers) that all work the same way: given a message of a specific type, blow the entire state away and return a new state with some value or values that are different to the previous state.
So
- The state is basically always a plain object. It can be a single primitive value or a single array or whatever – eg it could be a single number. But because Redux is designed for complex state, you’re unlikely to ever see this in practice outside of the very simplest of tutorial examples.
- To create a new state based on the previous state, you write a function which accepts two parameters. First, the current state. Second a plain object with a
type
property (and optionally any number of other properties). If the value of type
passed to the function matches what you want, return a new state, based on the current state (normally exactly the same shape but with some of the values modified).
- to keep hold of the current state, you use the function Redux provides (
createStore
), to which you pass your reducer. That looks like as I described – internally, it keeps hold of the current state, and returns some functions for reading and updating (if that didn’t exist, to keep updating the state you’d have to keep writing state = reducer(state, {type: "SOME_ACTION"})
all through your app which would sorta defeat the point of keeping it all in one place).
The thing I missed off the createStore
example is that it allows you to subscribe to updates to the store. The is another function, addListener
, which lets you add listeners, which just needs to be functions that take no arguments and do something. Every
time I update the store, every listener runs.
function createStore(reducer) {
let state;
let listeners = [];
return {
getState: () => {
return state;
},
dispatch: (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
return action;
},
addListener: (listener) => {
listeners.push(listener);
},
};
}
So say you have the example from my first post. You have the reducer, and the login
action, and you set up the store. But you also define two listener functions:
const store = createStore(reducer);
// store is now an object with three functions
// attached, getStore, addListener and dispatch.
// I'll define a couple of listener functions
const stateLogger = () => console.log(store.getState());
const sayHi = () => console.log("HI!");
Now:
> store.addListener(stateLogger)
> store.addListener(sayHi)
> store.dispatch(loginAction())
{ login: true }
HI!
> store.dispatch({ type: "WHATEVER"})
{ login: false }
HI!
> store.dispatch(loginAction())
{ login: true }
HI!
This is when it becomes more useful – libraries for SPA frameworks (for example react-redux
) use the addListeners
function to connect components up to the state. When a component is connected, it subscribes to the store. Every time you dispatch
, the listener function fires, which will update the component’s props.