Learn Redux by Building Redux

Redux, a popular name in the web engineering world, is a simple state management library. It was created by Dan Abramov and Andrew Clark in 2015. It can be used in any web application with any framework. That means we can use Redux with React, Vue, Angular, and even Vanilla JS. If we notice its npm download count, that is huge, more than 7M+ weekly downloads. Just think. How many apps are using this simple library! 🤯 So learning Redux is highly recommended when you want to be a web-app magician.
In this article, we'll learn the fundamental things about Redux. But, I strongly believe, you will understand a library better when you can replicate it. That's why here we'll try to build a Redux-like library (MiniRedux) after learning fundamentals about Redux. Let's kick it off 💥💥💥
Building blocks of Redux
In the following section, I'm going to discuss some of the important building blocks of Redux.
-
Store
-
Dispatch
-
Action
-
Reducer
-
Subscriber
-
Getter
-
Store is the global space where all the states are stored that can be accessed from all the sections of the app. It's a javascript object where each property-value pair is known as a slice.
-
Whenever we need to update a slice of the store, we have to call the dispatch() function with a specific action.
According to functional programming, we can't mutate data. If we have a store like,
store = {
count: 10,
user: "Showrin",
}
we can't simply update the value like the following.
store.count = 11;
According to the functional programming paradigm, we have to clone the store first, then update the count, and then use the new store.
const newStore = { ...store, count: 11 }
That's why we can't directly access the states inside the Redux store, can't directly update the Redux store. For reading the
stateswe have to usegetState()and for updating thestateswe have to usedispatch().
-
The dispatch function always expects an object which is called an Action. Usually, we declare an action object with a type and payload keys. It's not a rule from Redux but easy to read and maintain the codebase. Example of an action:
{ type: 'removeTask', payload: { id: 1, } } -
Reducer is the place where we can decide:
- Which state/slice should be updated
- How to calculate the value of a state
- What value should be assigned to a state
In reducer, we can keep all our business logic, and according to action type, we update a state/slice.
-
Subscriber is a higher-order function that receives a listener function as an argument and invokes this listener function whenever the store receives an update.
-
Earlier I said that we can't directly access states/slices inside the redux store. As an entry point, redux provides us a getter function
getState()for reading the states inside the store.
Flow of Updating Redux Store
Think about a very common scenario in daily store.
A store has many different products. Imagine this store as the redux store and different products as different key-value pairs (or slices) in the redux store.
If the shopkeeper wants to buy some products what should he do first? Yes, he has to decide which products to buy. Let's assume that he has decided to buy 20kg of sugar. Now, imagine this decision as an action. It can be written like:
{
type: 'buySugar',
payload: {
quantity: 20,
unit: 'kg',
}
}
After taking this decision, shop keeper will call or inform importer about his decision. Think it as dispatch() that receives an action (ex: buy sugar).
Once importer has decision from shop keeper, importer will start processing that request. Imagine importer as the reducer function.
Importer sends products to the shop keeper and shop-keeper loads the store with those products. Here actually what is happening is the store is updated.
Once the shop keeper updates the store, he will sends the message to all the subscribed-customers who are using products those are just updated. And yes, this is the last phase of the update-flow that is invoking listener functions provided to subscribe().

Can you relate the above scenario with Redux? I believe you can. To sum up the scenario, I'm explaining the update-flow in text-book language.
-
First of all, we have to define an action with
typekey. -
Then we have to invoke the dispatch() with that action to initiate an update.
-
Redux invokes the reducer function with latest state and the dispatched action. This reducer function returns the updated state based on the action-type and redux replace the old state with this new state.
-
After an update redux invokes the listener function that was passed as an argument to subscribe function.

Write a Simple Vanilla JS program using Redux
Whenever you're about to use redux in your app, first thing you have to do is decide the actions.
Here, I did the following tasks.
-
Define action-types (ex: ADD_TASK)
-
Define action with action-types and payload
-
Write the reducer function to return the updated state based on action-type
-
Create store with that reducer
-
Get the state from the reducer
-
Write a listener functions to log the latest state and pass it to subscribe()
-
Dispatch ADD_TASK action several times
-
Unsubscribe one of the listener functions
-
Dispatch ADD_TASK action some more times
If you notice the console, you'll see:
[]
[Object]
[Object, Object]
[Object, Object, Object]
[Object, Object, Object, Object]
As we subscribe a listener that prints the latest state, when the state/slice inside store gets changed, that's why we are getting this console.log().
After 4th dispatch, we unsubscribed a listener. That's why there is no console.log() of that listener happened after unsubscribing.
Write our own Redux
Yeah, I know 🍩 this is the most interesting part 🍩 of this article. So, let's build our own redux 🎉🎉🎉
Replicate createStore function
Redux gives us one vital method createStore and that is the first step of creating a redux store in any application.
Basically, createStore returns an object which is our store. This store object has some methods. If you log the store in the console, you'll see the following methods.
{
@@observable: ƒ observable(),
dispatch: ƒ dispatch(action),
getState: ƒ getState(),
replaceReducer: ƒ replaceReducer(nextReducer),
subscribe: ƒ subscribe(listener),
}
We won't write all of the methods. We'll write the following important methods.
-
getState
-
dispatch
-
subscribe
Let's create a file miniRedux.js and then write our first function createStore.
export function createStore (reducer) {
let state;
function getState () {}
function dispatch () {}
function subscribe () {}
return {
getState,
dispatch,
subscribe,
}
}
Explanation
-
We've created a function
createStorethat takesreduceras an argument. -
Inside the function, we declared a state variable and that is the global state of the app.
-
We've also created three empty functions
getState,dispatch,subscribe. We'll work on them in upcoming sections. -
Then we returned an object and expose those three functions as its method.
-
Finally, we did named-export the
createStorefunction to keep sync with thereduxlibrary.
Replicate getState function
In redux, when we write store.getState(), it returns the current global state.
It's a simple getter function to read the global state.
export function createStore (reducer) {
let state;
function getState () {
return state;
}
...
...
return { ... }
}
Explanation
-
Earlier we created a variable
state. In thegetStatefunction, we just returned thatstate. -
One thing to notice. There is no way to access the
statevariable exceptgetState, even no way to updatestate. In the next section, we'll work on updating thestate.
Replicate dispatch function
This section is the most important among all three methods of the redux store.
Since there is no way to update the state, we have to provide a function to update the state. This is the dispatch function that redux uses for updating the global state.
Let's have a small recap about the dispatch function. When we call the dispatch function we have to provide an action object as its argument. Then, dispatch calls the reducer function that was passed to createStore function. While calling the reducer, it passes the state and action as arguments of reducer. I think you can remember how we wrote the reducer.
function reducer (state=[], action) { ... }
See? What did we write in the parameter section? Yes, state and action. And those are passed by the dispatch function.
And lastly state gets updated with the value returned by the reducer.
I think this recap is enough for now. Let's write our dispatch function.
export function createStore (reducer) {
let state;
...
...
function dispatch (action) {
state = reducer(state, action);
}
...
...
return { ... }
}
Explanation
-
We called the
reducerfunction withstateandactionas arguments. -
The value (new state) returned by the reducer is assigned to the
state. Thus the globalstategets updated.
Replicate subscribe function
This section is a little bit complex. The only functionality left is the subscribe function. A listener function is passed to this function that is called after each state update.
The subscribe function returns a function unsubscribe. Once the unsubscribe function is called, the listener won't be called anymore on the state update.
One point to note, we can subscribe with multiple listeners and unsubscribe any of them independently. Let's have a look at the following example.
...
const unsubscribe1 = subscribe(listener1);
const unsubscribe2 = subscribe(listener2);
...
...
dispatch(...);
dispatch(...);
unsubscribe1();
dispatch(...);
dispatch(...);
...
Here we subscribe with two listeners
listener1, andlistener2. But after two dispatches, we unsubscribedlistener1. We calleddispatchfour times in total. Thelistener1will be called only 2 times(for the first two dispatches). Butlistener2will be called 4 times as it's not unsubscribed.
Let's write our subscribe function.
export function createStore (reducer) {
let state;
let listeners = [];
...
...
function dispatch(action) {
...
listeners.forEach((listener) => {
listener();
});
}
function subscribe(listener) {
let isSubscribed = true;
listeners.push(listener);
return function unsubscribe() {
if (!isSubscribed) {
return;
}
const listenerIndex = listeners.indexOf(listener);
isSubscribed = false;
listeners.splice(listenerIndex, 1);
};
}
return { ... }
}
Explanation
-
To keep track of whether a listener is subscribed or not, we declared a variable
isSubscribed. -
Then we added the
listenerfunction (from parameter) to our listeners' list. -
In the
unsubscribefunction we did two things.-
First we set
falsetoisSubscribed. IfisSubscribedis false,unsubscribefunction will stop being executed. -
And then, we find and removed the respective
listenerfrom thelisteners.
-
-
And at the end, we called all the
listenerfunctions insidedispatchafter updating thestate.If a listener is unsubscribed, we're removing it from the
listeners. As a result, it won't be called anymore insidedispatch.
Now just change the import from import { createStore } from "redux" to import { createStore } from "./miniRedux";. Here's the whole code for MiniRedux.
Conclusion
With this implementation, we've built our own redux 🏆 Cheers 🥂🥂 Yes, it doesn't have all mechanisms of redux. We didn't do any parameter-type checking here. But I believe, now we don't have that much fear about redux that we had earlier 💪💪 Because now we know how things work in redux. To know more details, go through the codebase of redux https://github.com/reduxjs/redux/blob/master/src/createStore.ts.
If you find this article helpful, feel free to share it with your network 🤝🤝🤝
Happy Coding 💻💻💥💥