Angular Icon Get 73% off the Angular Master bundle

See the bundle then add to cart and your discount is applied.

0 days
00 hours
00 mins
00 secs

Write Angular like a pro. Angular Icon

Follow the ultimate Angular roadmap.

Finally understand Redux by building your own Store

Redux is an interesting pattern, and at its core a very simple one - but why does it feel complex to understand? In this post, we’re going to dive into the core Redux concepts and understand the internal mechanics of a Store.

The benefit of this is to gain further understanding of the magic “under the hood” of Redux, the Store, reducers and actions - and how they all actually work. This helps us debug better, write better code, and know exactly what the code we write is actually doing. We’ll be learning all of this through assembling our own custom Store written in TypeScript.

This post is based off my “vanilla TypeScript Redux store”, you can grab the source code and follow along here if you wish. Please bear in mind, this is for learning purposes to understand the mechanics inside a Store - following the Redux pattern.

Terminology

If you’re new to Redux, or have flicked through the documentation, you’ll have likely come across a few of the following terms, which I think is worth covering before we begin.

Actions

Don’t try and think about actions as a JavaScript API, actions have a purpose - and we need to understand that first. Actions inform the Store of our intent.

You’re essentially passing an instruction, such as “Hey Store! I’ve got an instruction for you, please update the state tree with this new piece of information.”

The signature of an action, using TypeScript to demonstrate, is as follows:

interface Action {
  type: string;
  payload?: any;
}

Payload is an optional property, as sometimes we may dispatch some kind of “load” action which accepts no payload, though most of the time we’ll use the payload property.

This means that we’ll create something like this:

const action: Action = {
  type: 'ADD_TODO',
  payload: { label: 'Eat pizza,', complete: false },
};

That’s pretty much the blueprint of an action. Let’s continue!

Reducers

A reducer is simply a pure function which accepts the state of our application (our internal state tree, which our Store passes to the reducer), and finally a second argument of the action which was dispatched. Which means we end up with something like this:

function reducer(state, action) {
  //... that was easy
}

Okay, so what’s next to understand a reducer? The reducer gets passed our state as we know, and to do something useful (such as updating our state tree), we need to respond to the action’s type property (which we just looked at above). This is typically done via a switch:

function reducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO': {
      // I guess we should do something now...
    }
  }
}

Each case inside the switch allows us to respond to the different types of actions that compose state in our applications. For instance, let’s say we want to add a property with a value to our state tree, we’d simply return it:

function reducer(state = {}, action) {
  switch (action.type) {
    case 'ADD_TODO': {
      return {
        ...state,
        // we spread the existing todos array into a new array
        // and then add our new todo on the end
        todos: [...state.todos, { label: 'Eat pizza,', complete: false }],
      };
    }
  }

  return state;
}

Note at the bottom here, we’re returning state to pass the state back if we do not match a particular action. You’ll notice that I’ve added state = {} in the first argument (which is supplying a default value for the parameter). These initial state objects are typically abstracted above the reducer, and we’ll look at this as we continue.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

The final thing to note here, is our push for immutability. We’re returning a brand new object in each case, which reflects the new state tree changes, as well as the existing state tree representation - which means we have a slightly modified state object. The way we merge existing state is via the ...state, where we simply spread the current state in, and add additional properties after.

To honour the concept of pure functions, given the same input we return the same output each time. Reducers handle purely dynamic state and actions, in short we set them up - and they handle the rest. They’re encapsulated functions that simply contain the pieces of logic necessary to update our state tree, based on which type of instruction we are sending (via an action).

Reducers are purely synchronous, we should avoid asynchronous intent inside a reducer.

So where does the action.payload come into play? Ideally we wouldn’t hard-core values into a reducer, unless they were simple things like a boolean toggle from false to true. To complete our full circle trip of abiding by the “pure functions” rule, we access the action.payload property supplied in the function arguments to obtain any data we’ve dispatched via an action:

function reducer(state = {}, action) {
  switch (action.type) {
    case 'ADD_TODO': {
      // give me new data
      const todo = action.payload;
      // compose new data structure
      const todos = [...state.todos, todo];
      // return new state representation
      return {
        ...state,
        todos,
      };
    }
  }

  return state;
}

Store

One thing I see is confusion between “state” and “store”. Store is your container, state lives in the container. Store is an object with an API that lets you interact with your state, by modifying it, asking for its value - and so forth.

I think we’re about ready to begin building our custom Store, and all of these separate topics will start to click into place as we continue.

One thing I like to tell others is “this is just a structured process for updating a property on an object”. That is Redux.

Store API

Our example Redux Store is going to have just a few public properties and methods. We’ll then use our Store as follows, supplying any reducers and initial state for our application:

const store = new Store(reducers, initialState);

Store.dispatch()

The dispatch method will allow us to present an instruction to our Store, informing it that we intend to change the state tree. This is handled via our reducer, which we’ve just covered.

Store.subscribe()

The subscribe method will allow us to pass a subscriber function into our Store, which when our state tree changes, we can pass that new state tree changes down via an argument to our .subscribe() callback.

Store.value

The value property will be setup as a getter and return the internal state tree (so we can access properties).

Store Container

As we know, the Store contains our state, and also allows us to dispatch actions and subscribe to new state tree updates. So let’s start with our Store class:

export class Store {
  constructor() {}

  dispatch() {}

  subscribe() {}
}

This looks good for now, but we’re missing our “state” object. Let’s get it added:

export class Store {
  private state: { [key: string]: any };

  constructor() {
    this.state = {};
  }

  get value() {
    return this.state;
  }

  dispatch() {}

  subscribe() {}
}

I’m using TypeScript here, as I much prefer it, to define that our state object will be composed of keys of type string, with any value. Because that’s exactly what we need for our data structures.

We’ve also added the get value() {} which internally returns the state object, when accessed as a property, i.e. console.log(store.value);.

So now we’ve got this, let’s instantiate it:

const store = new Store();

Voila.

At this point we could actually call a dispatch if we wanted:

store.dispatch({
  type: 'ADD_TODO',
  payload: { label: 'Eat pizza', complete: false },
});

But it’s not going to do anything, so let’s dive into focusing on our dispatch and supplying that action:

export class Store {
  // ...
  dispatch(action) {
    // Update state tree here!
  }
  // ...
}

Okay, so inside the dispatch, we need to update our state tree. But first - what does our state tree even look like?

Our state data structure

For this article, our data structure will look like this:

{
  todos: {
    data: [],
    loaded: false,
    loading: false,
  }
}

Why? We’ve learned so far that reducers update our state tree. In a real app, we’ll have many reducers, which are responsible for updating specific parts of the state tree - which we often refer to as “slices” of state. Each slice is managed by a reducer.

In this case, our todos property on our state tree - the todos slice - is going to be managed by a reducer. Which at this point, our reducer will simply manage the data, loaded and loading properties of this slice. We’re using loaded and loading because when we perform asynchronous tasks such as fetching JSON over HTTP, we want to remain in control of the various steps it takes from initiating the request - to the request being fulfilled.

So, let’s jump back into our dispatch method.

Updating our state tree

In order to follow immutable update patterns, we should assign a new representation of state to our state property as a brand new object. This new object consists of any changes we’re intending to make to the state tree, via an action.

For this example, let’s ignore the fact that reducers even exist and simply update the state manually:

export class Store {
  // ...
  dispatch(action) {
    this.state = {
      todos: {
        data: [...this.state.todos.data, action.payload],
        loaded: true,
        loading: false,
      },
    };
  }
  // ...
}

After we’ve dispatched this 'ADD_TODO' action, our state tree now looks like this:

{
  todos: {
    data: [{ label: 'Eat pizza', complete: false }],
    loaded: false,
    loading: false,
  }
}

Writing Reducer functionality

Now we’ve got an understanding that a reducer updates a slice of state, let’s start by defining that initial slice:

export const initialState = {
  data: [],
  loaded: false,
  loading: false,
};

Creating a Reducer

Next up, we need to supply our reducer function that state argument, with a default value of the above initialState object. This sets up the reducer for initial load, when we invoke the reducer in the Store to bind all initial state, inside all of the reducers:

export function todosReducer(
  state = initialState,
  action: { type: string, payload: any }
) {
  // don't forget to return me
  return state;
}

We should probably be able to guess the rest of the reducer at this point:

export function todosReducer(
  state = initialState,
  action: { type: string, payload: any }
) {
  switch (action.type) {
    case 'ADD_TODO': {
      const todo = action.payload;
      const data = [...state.data, todo];
      return {
        ...state,
        data,
      };
    }
  }

  return state;
}

Okay, so this is great so far - but the reducer needs hooking up to the Store so we can invoke it to pass the state and any actions.

Back inside the Store, we should have this so far:

export class Store {
  private state: { [key: string]: any };

  constructor() {
    this.state = {};
  }

  get value() {
    return this.state;
  }

  dispatch(action) {
    this.state = {
      todos: {
        data: [...this.state.todos.data, action.payload],
        loaded: true,
        loading: false,
      },
    };
  }
}

We now need to hook in the ability to add reducers to the Store:

export class Store {
  private state: { [key: string]: any };
  private reducers: { [key: string]: Function };

  constructor(reducers = {}, initialState = {}) {
    this.reducers = reducers;
    this.state = {};
  }

}

We’re also supplying any initialState to the Store, so we can supply this when we invoke the Store should we wish to.

Registering a Reducer

To register a reducer, we must remember that todos property on our expected state tree - and bind our reducer function to it. Remember, we’re managing a slice of state called “todos”:

const reducers = {
  todos: todosReducer,
};

const store = new Store(reducers);

This is the magic piece where the property todos is then the result of the Store invoking the todosReducer - which as we know returns new state based on a particular action.

Invoking Reducers in the Store

The reason reducers are called “reducers” is because they reduce new state. Think Array.prototype.reduce, where we end up with one final value. In our case, this final value is the new representation of state. Sounds like we need a loop.

What we’re going to do is wrap our “reducing” logic in a function, which here I’ve called reduce:

export class Store {
  // ...
  dispatch(action) {
    this.state = this.reduce(this.state, action);
  }

  private reduce(state, action) {
    // calculate and return new state
    return {};
  }
}

When we dispatch an action, we’ll in fact call the reduce method we’ve created on the Store class - and pass the state and action inside. This is actually called the root reducer. You’ll notice it takes the state and action - much like our todosReducer also does.

So, let’s dive into our private reduce method, because this is the most important step for the composition of our state tree to fully click.

export class Store {
  private state: { [key: string]: any };
  private reducers: { [key: string]: Function };

  constructor(reducers = {}, initialState = {}) {
    this.reducers = reducers;
    this.state = {};
  }

  dispatch(action) {
    this.state = this.reduce(this.state, action);
  }

  private reduce(state, action) {
    const newState = {};
    for (const prop in this.reducers) {
      newState[prop] = this.reducers[prop](state[prop], action);
    }
    return newState;
  }
}

What’s happening here is:

The prop value in this case, is just todos, so you can think of it like this:

newState.todos = this.reducers.todos(state.todos, action);

Reducing initialState

There’s one final piece, our initialState object. If you want to use the Store(reducers, initialState) syntax to provide store-wide initial state, we need to reduce it as well upon Store creation:

export class Store {
  private state: { [key: string]: any };
  private reducers: { [key: string]: Function };

  constructor(reducers = {}, initialState = {}) {
    this.reducers = reducers;
    this.state = this.reduce(initialState, {});
  }

  // ...
}

Remember when we talked about return state at the bottom of each reducer? Now you know why! We have this option to pass {} as the action, meaning the switch cases will be avoided - and we end up with a state tree we supply through the constructor.

Enabling subscribers

You’ll often hear the term “subscribers” in the Observable world, where each time an Observable emits a new value, we are notified via a subscription. A subscription is simply “give me data when it’s available, or changes”.

In our case, this would be handled like so:

const store = new Store(reducers);

store.subscribe(state =&gt; {
  // do something with `state`
});

Store Subscribers

Let’s add a few more properties to our Store to allow us to setup this subscription:

export class Store {
  private subscribers: Function[];

  constructor(reducers = {}, initialState = {}) {
    this.subscribers = [];
    // ...
  }

  subscribe(fn) {}

  // ...
}

Here we have our subscribe method, which now accepts a function (fn) as the argument. What we need to do is pass each function into our subscribers array:

export class Store {
  // ...

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
  }

  // ...
}

That was easy! So where does it make sense to inform our subscribers that something changed? In the dispatch of course!

export class Store {
  // ...

  get value() {
    return this.state;
  }

  dispatch(action) {
    this.state = this.reduce(this.state, action);
    this.subscribers.forEach(fn => fn(this.value));
  }

  // ...
}

Again, super easy. Any time we dispatch, we reduce the state and loop our subscribers - and pass in this.value (remember that’s our value getter).

Buuuuuuut, there’s just one more thing. When we call .subscribe() we won’t (at this point in time) get the state value right away. We’ll only get it after we dispatch. Let’s make a concious decision to inform new subscribers of the current state, as soon as they subscribe:

export class Store {
  // ...

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
    fn(this.value);
  }

  // ...
}

That was also nice and easy - we get given fn - the function - via the subscribe method, and we can just simply invoke that function as soon as we subscribe, and pass the value of the state tree in.

Unsubscribing from the Store

When we subscribe, we always want to be able to unsubscribe - for purposes such as avoiding memory leaks, or simply because we don’t care about the data anymore.

All we need to do is return a function closure, which when invoked will unsubscribe us (by removing the function from our list of subscribers):

export class Store {
  // ...

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
    fn(this.value);
    return () => {
      this.subscribers = this.subscribers.filter(sub => sub !== fn);
    };
  }

  // ...
}

We simply use the function’s reference, iterate our subscribers, check if the current subscriber does not equal our fn, and by using Array.prototype.filter, it magically is removed from our subscribers array.

And we can use it as follows:

const store = new Store(reducers);

const unsubscribe = store.subscribe(state => {});

destroyButton.on('click', unsubscribe, false);

And that’s all we need.

The beauty of subscriptions is we can also have multiple subscribers, meaning different parts of our application are interested in different slices of state.

Final Code

Here’s the full picture and finished solution:

export class Store {
  private subscribers: Function[];
  private reducers: { [key: string]: Function };
  private state: { [key: string]: any };

  constructor(reducers = {}, initialState = {}) {
    this.subscribers = [];
    this.reducers = reducers;
    this.state = this.reduce(initialState, {});
  }

  get value() {
    return this.state;
  }

  subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
    fn(this.value);
    return () => {
      this.subscribers = this.subscribers.filter(sub => sub !== fn);
    };
  }

  dispatch(action) {
    this.state = this.reduce(this.state, action);
    this.subscribers.forEach(fn => fn(this.value));
  }

  private reduce(state, action) {
    const newState = {};
    for (const prop in this.reducers) {
      newState[prop] = this.reducers[prop](state[prop], action);
    }
    return newState;
  }
}

You can see that in reality, there isn’t much going on here.

Wrapping up

That wasn’t so bad was it? We’ve likely used/seen/heard about all these different terminology examples, but haven’t necessarily dived underneath to think about how they are composed.

We’ve finally understood what a Store does for us, by creating our own. It’s taking the magic away from simply creating an action, reducer, and just letting it “work”. We fully grasped the concepts and mechanics of what’s happening; our dispatch tells the Store to carry out a process of defining new state by invoking each reducer and attempting to match our action.type with a switch case. Our state tree is simply a final representation of having invoked all of our reducers.

For me, this was the biggest part in understanding Redux, and I hope it’s helped you on your way too!

You can take this one step further with my NGRX course for Angular, to learn how to fully master state management with NGRX Store and Effects.

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover