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
states
we have to usegetState()
and for updating thestates
we 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
type
key. -
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
createStore
that takesreducer
as 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
createStore
function to keep sync with theredux
library.
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 thegetState
function, we just returned thatstate
. -
One thing to notice. There is no way to access the
state
variable 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
reducer
function withstate
andaction
as arguments. -
The value (new state) returned by the reducer is assigned to the
state
. Thus the globalstate
gets 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 calleddispatch
four times in total. Thelistener1
will be called only 2 times(for the first two dispatches). Butlistener2
will 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
listener
function (from parameter) to our listeners' list. -
In the
unsubscribe
function we did two things.-
First we set
false
toisSubscribed
. IfisSubscribed
is false,unsubscribe
function will stop being executed. -
And then, we find and removed the respective
listener
from thelisteners
.
-
-
And at the end, we called all the
listener
functions insidedispatch
after 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 💻💻💥💥