Understanding Redux by doing a basic implementation of the store

5 min read

Understanding Redux by doing a basic implementation of the store

In this post we will be re-writing a simplified version of Redux store to understand how it works.

Store

Store is the central repository that holds the state of your application.

CreateStore

First we create the function createStore() that is going to return the store which contains 4 parts

1
1. The store state
2
2. getState
3
3. subscribe (listening to changes on the state)
4
4. dispatch (updating the state)

Whenever you invoke createStore() you going to get back an object that represent the store which contains:

1
1. a way to getting state.
2
2. listening to changes on the state.
3
3. updating the state

As for the state, its not going to be publicly available but you can interact with it by using the public methods that we mentioned above.

So first we need some container that holds the state internally which the store can modify or access it using the 3 methods.

1
function createStore() {
2
let state;
3
}

getState()

Now that this state is locally available to the function createStore(), we need to create a way to interact with this state.

1
function createStore() {
2
let state;
3
4
function getState() {
5
return state;
6
}
7
8
return {
9
getState,
10
};
11
}

This way when a user create a store and want to get an access to the state they can do that by invoking getState().

subscribe()

The next step is to make the user listen to the changes on the state.

We do that by making a variable called listeners that is going to be an empty array initially. And whenever subscribe() is called its going to receive the listener callback function. And inside it we call listeners which is the array we have created and push the listener callback function that was received when subscribe was invoked to that array.

1
function createStore() {
2
let state;
3
let listeners = [];
4
function getState() {
5
return state;
6
}
7
8
function subscribe(listener) {
9
listeners.push(listener);
10
}
11
return {
12
getState,
13
subscribe,
14
};
15
}

So inside createStore() we need to keep track of anytime the user calls store.subscribe() and keep track of these functions passed in so that whenever the state changes we go through the list of functions and invoke each of them.

1
function createStore() {
2
let state;
3
let listeners = [];
4
function getState() {
5
return state;
6
}
7
function subscribe(listener) {
8
listeners.push(listener);
9
10
return function () {
11
listeners = listeners.filter(function (listenerItem) {
12
return listenerItem !== listener;
13
});
14
};
15
}
16
17
return {
18
getState,
19
subscribe,
20
};
21
}

when the user call .subscribe it will return a function (unsubscribe function) and when this function is invoked we want to go ahead and remove the listener that was passed in to subscribe from our listeners array.

In other words, when the user call .subscribe() and pass a listener function it will be pushed to the listeners array and they will get back a function as a result of invoking .subscribe(). If they invoke the returned function it will filter the listeners array and remove the listener function.

##dispatch() Next will be updating the store state, as we want to increase predictability, we would want to specify the sort of events that could update the state, these events are called actions. An action is just an object that represent an event that is going to change the state of our store. Action must have a type property to specifiy the type of action occuring, other than that the structure is up to you.

1
// action example
2
{
3
  type: 'ADD_TODO',
4
  payload: {
5
todo: {
6
id: 0,
7
name: 'Write a post about Redux'
8
}
9
  }
10
}

when action occurs inside of our application we want to take the payload from the incoming action and add it to our application state.

This would be done inside the reducer function which is a pure function that takes in the current state and action and based on the action type it will specify how the state gets changed.

So now whenever we invoke dispatch(), it will take in the action and inside it we call the reducer function that is taking the current state and the action and is going to get us the new state which then we can update internally.

Whenever we update the state we need loop through the listeners and invoke them so that they know that the state has been updated.

1
function dispatch(action) {
2
state = reducer(state, action);
3
listeners.forEach(function (listener) {
4
return listener();
5
});
6
}

and here is the whole store put together

1
function createStore(reducer) {
2
let state;
3
let listeners = [];
4
5
function getState() {
6
return state;
7
}
8
9
function subscribe(listener) {
10
listeners.push(listener);
11
12
return function () {
13
listeners = listeners.filter(function (listenerItem) {
14
return listenerItem !== listener;
15
});
16
};
17
}
18
19
function dispatch(action) {
20
state = reducer(state, action);
21
listeners.forEach(function (listener) {
22
return listener();
23
});
24
}
25
26
return {
27
getState,
28
subscribe,
29
dispatch,
30
};
31
}

and the following is an example usage of store methods

1
// reducer function - example
2
function todos(state = [], action) {
3
if (action.type === "ADD_TODO") {
4
return state.concat([action.payload]);
5
}
6
return state;
7
}
8
9
// create new store
10
const store = createStore(todos);
11
12
// to listen to the changes - something like a logger
13
const unsubscribe = store.subscribe(() => {
14
console.log("state: ", store.getState());
15
});
16
17
// to update the state
18
store.dispatch({
19
type: "ADD_TODO",
20
payload: {
21
todo: {
22
id: 0,
23
name: "Write a post about Redux",
24
},
25
},
26
});