loading...
Cover image for Redux in 27 lines

Redux in 27 lines

selbekk profile image selbekk Updated on ・6 min read

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:

  1. The user dispatches an action
  2. The action is passed to your reducer
  3. The reducer returns the new state
  4. The state is updated in the store
  5. 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!

Posted on Mar 21 '19 by:

selbekk profile

selbekk

@selbekk

I'm a full stack dev that specializes in React, English Bulldogs and beer based coding meetups

Discussion

markdown guide
 
 

Great article! I never dived deep into Redux's core logic but this is really fascinating.

Would you mind checking my last article about middlewares? Maybe you can give me some good advices!

dev.to/pigozzifr/lets-play-with-re...

 

Thanks for a very helpful article. I have dabbled with Redux before, and while I think I had a decent grasp of the concept, seeing how it actually works, and how simple the mechanisms are really makes it a lot easier to wrap my head around.

 

Last time I played with Redux, there was the concept of smart and dumb components, and smart components had to be connected to Redux. Your article seems to indicate just adding subscribers, which is slick.. but.. how do you manage re-rendering only components that are connected to stores/actions so that for example, you can have a component (or subset of components) re-render only when a specific action/state occurs, and not have the state call all subscribers on all changes? Is that no longer the way Redux works? Or is that more advanced and this intro was to keep it simple?

 

This intro is actually pretty framework agnostic - talking about how Redux is implemented, not how it connects to React.

That being said, the way the connect method from react-redux works is by subscribing on mount, unsubscribing on unmount, and implementing a clever shouldComponentUpdate based on the output of your mapStateToProps method. You could create a similar HOC with this implementation, too!

 

I love the process of what Redux is and how to implement it step by step as it helps understand how Redux works 🙂

 

For anyone interested in redux, reading the source really is a great way to see what it can do.

It is surprisingly small, and fast to read through, as this article indicates. :)