TypeOfNaN

Learn the Basics of Redux by Writing Your Own Version in 30 Lines

Nick Scialli
September 12, 2020

redux logo

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 a payload 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 and dispatch.

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:

  1. We have a centralized way to access our stateful information (by calling store.getState())
  2. 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!

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli