selbekk

Redux in 27 lines

Redux in 27 lines

March 21, 2019
6 min read

Ever wondered how Redux works? Here's how you can implement your own!

Redux has become the defacto standard for state management in React. It's a great tool for handling global state, and its sheer popularity means you'll probably want to learn it at some point.

Redux isn't the easiest concept to learn though. Even though the docs are good (and are being rewritten to be even better), it's often hard to understand the concept of Redux' uni-directional data flow, dispatching, reducing, actions and what have you. I struggled with it myself, when I first came across Redux.

Luckily for us, Redux isn't as complicated as it looks. As a matter of fact, you can implement a working version of the core parts or Redux in 27 lines of code!

This article will take you through how you can implement an API similar to Redux yourself. Not because you'll want to do just that, but because it'll help you understand how Redux works!

What is Redux, really? 🤔

The core part of Redux the store. This store contains a single state tree. The store lets you read the state, dispatch actions to update the state, subscribe and unsubscribe for updates to that state, that's about it.

This store is passed around your application. If you're using React, you're probably passing your store to react-redux's <Provider /> component, which lets you access it in other parts of your application by wrapping your component with connect().

Let's implement Redux!

We're going to re-implement Redux by implementing the createStore method. It does what it says on the tin - it gives us a store instance we can play with. The store is just an object with a few methods on it, so it doesn't need to be fancy.

Step 1: getState

Let's start off small, by implementing the getState method:

function createStore() {
  let state = {};
  return {
    getState() {
      return state;
    }
  };
}

When we call createStore, we create an empty state object. This is that single state tree you keep hearing about. We return our "store", which is just an object with one property - a getState function. Calling this getState function grants access to the state variable inside of the createStore closure.

This is how we'd use it:

import { createStore } from './redux';

const store = createStore();
const state = store.getState();

Step 2: Accept a reducer

One of the core concepts of Redux is the reducer. A Redux reducer is a function that accepts the current state and an action, and returns the next state (the state after an action has happened). Here's a simple example:

function countReducer(state = 0, action) {
  if (action.type === 'INCREMENT') return state + 1;
  if (action.type === 'DECREMENT') return state - 1;
  return state;
}

Here - the countReducer responds to two actions - INCREMENT and DECREMENT. If the action passed doesn't match either, the current state is returned.

To continue our journey in understanding Redux, we need to take a quick break, and understand the data flow of Redux:

  • The user dispatches an action
  • The action is passed to your reducer
  • The reducer returns the new state
  • The state is updated in the store
  • Anyone interested in the new state gets notified.

In order for us to follow this flow, we need our store to have a reducer! Let's pass that in as the first argument:

function createStore(initialReducer) {
  let reducer = initialReducer;
  let state = reducer({}, { type: '__INIT__' });
  return {
    getState() {
      return state;
    }
  };
}

Here, we accept a reducer, and call it to get our initial state. We "trigger" an initial action, and pass in an empty object to our state.

Redux actually lets us pass in pre-calculated state when we create our store. This might have been persisted in local storage, or come from the server side. Anyhow, adding support for it is as simple as passing an initialState argument to our createStore function:

function createStore(initialReducer, initialState = {}) {
  let reducer = initialReducer;
  let state = reducer(initialState, { type: '__INIT__' });
  return {
    getState() {
      return state;
    }
  };
}

Great! Now we even support server side rendering - that's pretty neat!

Step 3: Dispatch actions!

The next step in our Redux journey is to give the user some way to say that something happened in our app. Redux solves this by giving us a dispatch function, which lets us call our reducer with an action.

function createStore(initialReducer, initialState = {}) {
  let reducer = initialReducer;
  let state = reducer(initialState, { type: '__INIT__' });
  return {
    getState() {
      return state;
    },
    dispatch(action) {
      state = reducer(state, action);
    }
  };
}

As we can tell from the implementation, the concept of "dispatching" an action is just calling our reducer function with the current state and the action we passed. That looks pretty simple!

Step 4: Subscribing to changes

Changing the state isn't worth much if we have no idea when it happens. That's why Redux implements a simple subscription model. You can call the store.subscribe function, and pass in a handler for when the state changes - like this:

const store = createStore(reducer);
store.subscribe(() => console.log('The state changed! 💥', store.getState()));

Let's implement this:

function createStore(initialReducer, initialState = {}) {
  let reducer = initialReducer;
  let subscribers = [];
  let state = reducer(initialState, { type: '__INIT__' });
  return {
    getState() {
      return state;
    },
    dispatch(action) {
      state = reducer(state, action);
      subscribers.forEach(subscriber => subscriber());
    },
    subscribe(listener) {
      subscribers.push(listener);
    }
  };
}

We create an array of subscribers, which starts out as empty. Whenever we call our subscribe function, the listener is added to the list. Finally - when we dispatch an action, we call all subscribers to notify them that the state has changed.

Step 5: Unsubscribing to changes

Redux also lets us unsubscribe from listening to state updates. Whenever you call the subscribe function, an unsubscribe function is returned. When you want to unsubscribe, you would call that function. We can augment our subscribe method to return this unsubscribe function:

function createStore(initialReducer, initialState = {}) {
  let reducer = initialReducer;
  let subscribers = [];
  let state = reducer(initialState, { type: '__INIT__' });
  return {
    getState() {
      return state;
    },
    dispatch(action) {
      state = reducer(state, action);
      subscribers.forEach(subscriber => subscriber());
    },
    subscribe(listener) {
      subscribers.push(listener);
      return () => {
        subscribers = subscribers.filter(subscriber => subscriber !== listener);
      };
    }
  };
}

The unsubscribe function removes the subscriber from the internal subscriber-registry array. Simple as that.

Step 6: Replacing the reducer

If you're loading parts of your application dynamically, you might need to update your reducer function. It's not a very common use-case, but since it's the last part of the store API, let's implement support for it anyways:

function createStore(initialReducer, initialState = {}) {
  let reducer = initialReducer;
  let subscribers = [];
  let state = reducer(initialState, { type: '__INIT__' });
  return {
    getState() {
      return state;
    },
    dispatch(action) {
      state = reducer(state, action);
      subscribers.forEach(subscriber => subscriber(state));
    },
    subscribe(listener) {
      subscribers.push(listener);
      return () => {
        subscribers = subscribers.filter(subscriber => subscriber !== listener);
      };
    },
    replaceReducer(newReducer) {
      reducer = newReducer;
      this.dispatch({ type: '__REPLACE__' });
    }
  };
}

Here we simply swap the old reducer with the new reducer, and dispatch an action to re-create the state with the new reducer, in case our application needs to do something special in response.

Step 7: What about store enhancers?

We've actually left out a pretty important part of our implementation - store enhancers. A store enhancer is a function that accepts our createStore function, and returns an augmented version of it. Redux only ships with a single enhancer, namely applyMiddleware, which lets us use the concept of "middleware" - functions that let us do stuff before and after the dispatch method is called.

Implementing support for store enhancers is 3 lines of code. If one is passed - call it and return the result of calling it again!

function createStore(initialReducer, initialState = {}, enhancer) {
  if (enhancer) {
    return enhancer(createStore)(initialReducer, initialState);
  }
  let reducer = initialReducer;
  let subscribers = [];
  let state = reducer(initialState, { type: '__INIT__' });
  return {
    getState() {
      return state;
    },
    dispatch(action) {
      state = reducer(state, action);
      subscribers.forEach(subscriber => subscriber(state));
    },
    subscribe(listener) {
      subscribers.push(listener);
      return () => {
        subscribers = subscribers.filter(subscriber => subscriber !== listener);
      };
    },
    replaceReducer(newReducer) {
      reducer = newReducer;
      this.dispatch({ type: '__REPLACE__' });
    }
  };
}

Step 8? There is no step 8!

That's it! You've successfully re-created the core parts of Redux! You can probably drop these 27 lines into your current app, and find it working exactly as it is already.

Now, you probably shouldn't do that, because the way Redux is implemented gives you a ton of safeguards, warnings and speed optimizations over the implementation above - but it gives you the same features!

If you want to learn more about how Redux actually works, I suggest you have a look at the actual source code. You'll be amazed at how similar it is to what we just wrote.

Takeaways

There isn't really any point in re-implementing Redux yourself. It's a fun party trick, at best. However, seeing how little magic it really is will hopefully improve your understanding of how Redux works! It's not a mysterious black box after all - it's just a few simple methods and a subscription model.

I hope this article has solidified your knowledge on Redux and how it works behind the scenes. Please let me know in the comments if you still have questions, and I'll do my best to answer them!

All rights reserved © 2024