Learn the Basics of Redux by Writing Your Own Version in 30 Lines
Nick Scialli
September 12, 2020
One of my favorite ways to learn how something works is to recreate it. In this post, we’re going to learn the basics of Redux by creating a simplistic version of it.
What Does Redux Accomplish?
Before we dive in, it’s important to understand what Redux accomplishes. Redux is a state management library. It helps you manage stateful information in an application. “Stateful information” is just a fancy way of saying information that needs to persist and be available during your application’s use. This includes things like a user’s name or whether the application is in “light” mode or “dark” mode.
State management libraries like Redux become especially useful when your application gets larger in size. Many people think Redux is a part of React or explicitly associated with React, but it’s actually its own standalone library and can be used with or without React.
The Basic Principles of Redux
The basic idea behind Redux is that you have a centralized location for your stateful information and can predictably update state. To accomplish this, Redux has the following basic structure:
- A state object - The state object contains the stateful information for your application. This could be information like the logged in user’s name and whether they’re in “light” or “dark” mode.
- Actions - Actions are objects that give Redux the information necessary to update state. By convention, an action object might have a
type
property and apayload
property. If you wanted to set the user’s name to “Frankie”, your action might look like this:{ action: "SET_USER_NAME", payload: "Frankie" }
- A reducer - Reducers are functions. They take two arguments: 1) the current state, 2) an action object (as described above). The reducer uses the information provided in the action object along with the current version of the state and returns a new version of the state.
- The store - The store is an object that allows you to access the current version of the state and also allows you to dispatch actions to update that state. The store object therefore has two properties, both of which are functions:
getState
anddispatch
.
Yikes, Am I Supposed to Understand All That?
One of Redux’s biggest criticism is that it has a steep learning curve, so you definitely should not be concerned if you don’t understand all of that. As we implement our own, stripped down version of Redux, these concepts should hopefully start clicking. And what really helps is actually using Redux in the wild!
Rolling Our Own Redux
Let’s get started with rolling our own Redux! If you’ve used Redux bofore, you know that you generally create your store
with a createStore
function provided by the library. We’re going to write this ourselves!
As I mentioned above, our store needs to allow us to access our state
object by using a getState
function. It also has to allow us to dispatch
actions. Let’s create a skeletal createStore
function based on this knowledge.
function createStore() {
let state = {}; // Don't know what this is yet
function getState() {
return state;
}
function dispatch(action) {
// Set state based on the action
}
return { getState, dispatch };
}
That’s a pretty good start! Let’s make some improvements. First off, we don’t always want our initial state
to be an empty object {}
. Instead, we’ll have createStore
take an argument called initialState
.
Next, our dispatch
funciton has to do something with the action
we passed it so that our state can be updated. The reducer
, as described above, fits this need:
Reducers are functions. They take two arguments: 1) the current state, 2) an action object (as described above). The reducer uses the information provided in the action object along with the current version of the state and returns a new version of the state.
So let’s pass our current state
object to the reducer along with the action
and set our state variable equal to the return value.
Here are both of our enhancements implemented:
function createStore(reducer, initialState) {
let state = initialState;
function getState() {
return state;
}
function dispatch(action) {
state = reducer(state, action);
}
return { getState, dispatch };
}
And that ends up actually being it for our simplified createStore
function! More experienced Redux users might notice that we’re omitting the third parameter from createStore
. This parameter becomes important as you get into more advanced Redux, but for core principles we’ll stick to this first two params!
Before we can use our createStore
function, we’ll need a reducer
. Let’s create a reducer that can either set a user name or set the display mode (light/dark).
As we’ve discussed, our reducer
function takes the current state
and an action
as arguments and returns a new version of the state.
function reducer(state, action) {
switch (action.type) {
case 'SET_USER_NAME':
return {
...state,
name: action.payload,
};
case 'SET_DISPLAY_MODE':
return {
...state,
displayMode: action.payload,
};
default:
return state;
}
}
Let’s dissect what we’ve done here.
Our reducer
takes a state
argument and an action
argument. We have a switch
statement that will return different things based on the value of action.type
(remember we discussed before that, by convention, our action
object has a type
and a payload
).
If action.type
is "SET_USER_NAME"
, then we return a copy of our state but we overwrite the name
key of state with the provided action.payload
. Conversely, if action.type
is "SET_DISPLAY_MODE"
, we return a copy of our state but we overwrite the displayMode
key. If the action.type
isn’t one of those two strings, we just return our state unmodified.
This is pretty much all we need, we can now take our home-rolled Redux for a test run!
A Test Run
Here’s a test run of our home-rolled Redux library. See inline comments for the play-by-play.
// The createStore function we already wrote
function createStore(reducer, initialState) {
let state = initialState;
function getState() {
return state;
}
function dispatch(action) {
state = reducer(state, action);
}
return { getState, dispatch };
}
// The reducer we already wrote
function reducer(state, action) {
switch (action.type) {
case 'SET_USER_NAME':
return {
...state,
name: action.payload,
};
case 'SET_DISPLAY_MODE':
return {
...state,
displayMode: action.payload,
};
default:
return state;
}
}
// Create a new store! This will take our reducer
// and also an initial version of our state.
const initialState = { name: 'Guest', displayMode: 'light' };
const store = createStore(reducer, initialState);
// Change our user's name to "Frankie"
store.dispatch({
type: 'SET_USER_NAME',
payload: 'Frankie',
});
console.log(store.getState());
//{ name: "Frankie", displayMode: "light" }
// Change our display mode to "dark"
store.dispatch({
type: 'SET_DISPLAY_MODE',
payload: 'dark',
});
console.log(store.getState());
//{ name: "Frankie", displayMode: "dark" }
That’s Pretty Much It
Now we have this pretty nifty store
object that accomplishes everything we wanted:
- We have a centralized way to access our stateful information (by calling
store.getState()
) - We have a repeatable, predictable way to update our stateful information by dispatching actions (by calling
store.dispatch(action)
).
I hope you enjoyed this introduction to Redux!
Nick Scialli is a senior UI engineer at Microsoft.