Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris
Redux is a Pub-Sub implementation but adds some new artifacts to the pattern by adding concepts such as immutability and an idea that only certain artifacts should be able to change the state called reducers
This article is part of a series:
- NGRX — from the beginning, part I, Pub-sub,
- NGRX — from the beginning, part II, we are here
- NGRX — from the beginning, part III, NGRX store, this covers basic usage of the Store
- NGRX — from the beginning, part IV, NGRX effects, in progress
- NGRX — from the beginning, part V, NGRX entity, in progress
Ok, we’ve learned about the Pub-Sub pattern in our last part so thereby we got to understand the underlying pattern to it all and when to use it. We even looked at an implementation. Now we will look at a specific version of the Pub-Sub pattern called Redux
In this article we will cover the following:
- The basic concepts, Redux consists of some basic concepts so let’s list what they are and their responsibility
- Actions, let’s go into what an Action is, when it’s used and how to create one
- Reducers, reducers are functions that guard and change to state and they also lead to a change of that same state but in an orderly and pure way
- Store, the store is the main thing we interact with when we either want to know what the state is or we want to change it
- Naive implementation, let’s look at how we can implement Redux so we really understand what is going on
Why Redux
In our first part, we looked at the Pub-Sub pattern. We understood, hopefully, that this was a pattern that was great to use if you needed to change data and broadcast that change to a number of listeners. So why Redux, why do we need this specific version of Pub-Sub. Well, Redux is opinionated on some things:
- There should be a single source of truth, one place where all your state lives, not many
- Changes should be carried out in an immutable way, this is considered more safe and predictable
- Changes can only be carried out with the help of Reducers
So what Redux adds to the table is being the answer to a set of problems. What problems are those, you ask?
The following:
- Disagreement on the current state, When your application grows it starts to have a problem of managing the state over the application. Many components want the same state and suddenly one or more components change the state and for some reason on or more components don't fully realize that a changed has happened so they are in disagreement on what the state should be
- Who did what, not only are you having a problem with components not agreeing about what the state should be but you have lost track of who did what, what caused a specific state to end up that way? The reason you have this problem is most likely because you let any components mutate the state directly, there is no guard that carries out the state in an orderly way that makes sure to update the interested parties
The basic concepts
Ok, so there are some concepts we need to know about to be able to grasp Redux properly. We’ve mentioned them in the formers section but let’s discuss them some more:
- Action, so the action is a message we send, it’s an intention, something we want like adding a product to a list for example. An action has a type, which is a verb representing our intention and it optionally has a payload, data that we need to carry our a change or use to query
- Reducer , a reducer is a function. The purpose of the reducer is to take an existing state and apply an action to the existing state. Reducers carry this out in an immutable way which means that we don’t mutate the state but rather compute a new state based on the old state and the action
- Store , the store is like a container holding the state. The store is like the API of Redux, the main actor that you talk to when you want to read state, change state or maybe subscribe to state changes
Ok, now we know a little more about the core concepts and what their role is. Let’s look at each concept more in detail with code example, cause we understand better if we see some code, right? ;)
Actions
Actions are the way we express what we want to do. An action has one mandatory property type and one optional property payload. An Action is an object, so an example action can look like this:
const action = { type: 'INCREMENT' };
The above is a simpler action that doesn’t have a payload, cause it’s not needed, the intention or the type says clearly what needs doing, increment by one. However, if you wanted to increase it by 2 using such an action you would need to describe that using a payload, like so:
const action = { type: 'INCREMENT', payload: 2 };
A more common case for an action, with a payload, would be adding a product, it would look like so:
const action = {
type: '[Product] add',
payload: { id: 1, name: 'movie' }
};
Ok, so we understand actions a little better but how do we apply them to an existing state? For that, we need to discuss reducers.
Reducers
Reducers are simple functions that carry out changes in an immutable way. Instead of mutating the state they are computing the state. Ok, sounds weird, let’s look at a mutating example first and explain why that’s bad:
let value = 0;
function add(val, val2) {
value += val + val2;
}
add(1,2);
add(1,2);
Above we can see that when we run the add()
function two times with the same input parameters we get different results. In a small contained example like this we can easily see why that is, we have variable value declared and we can also see that the implementation of add()
function uses value as part of its calculation. In a more realistic scenario, this might not be so easy to detect as the function might be many many rows long and contain a lot of complex things. This is bad because it isn't predictable and what we mean by that is that we can't easily see what the outcome of the function would be, give two parameters without first knowing the value of the variable value. A more predictable version of the add()
method would be:
function add(lhs, rhs) {
return lhs + rhs;
}
add(1,2);
add(1,2);
As we can see from the above code execution, given the same value on the input parameters we get the same outcome, it’s predictable.
Now remember this principle and let’s apply that on a reducer whose job it is to handle operations on a list. Let’s look at some code:
function reducer(state = [], action) {
switch(action.type) {
case '[Product] add':
return [...state, action.payload]
default: return state;
}
}
Looking at the above code we see that instead of calling the push()
method on the list we use a spread operator and constructs an entirely new list based on our existing list state and the new item being stored on action.payload. Let's invoke this reducer()
function:
let state = reducer( [], {
type: '[Product] add',
payload: { name: 'movie'}
});
state = reducer( state, {
type: '[Product] add',
payload: { name: 'book'}
});
What we can see from the above invocation is that we are able to keep on adding items to our list if we assign the result of reducer()
function invocation to the variable state. Furthermore, we also note how reducer()
does any addition to the list by computing:
old state + action = new state
This is an important principle in React and immutable functions how we change things so remember the above statement.
Store
Ok, we have understood so far that actions are the message that we send when we want to read data or change the data, in the state. So where is our state? It is stored in a store. Ok, so how do we communicate with the store? We do so by sending a message to it using the method dispatch()
. Let's try to start sketching on a store implementation:
class Store {
dispatch(action) {}
}
Ok, that wasn’t much, let's see if we can improve this a bit. What do we know? We know that any state change should happen because we send an action to the dispatch() method, but we also know that any state change is only allowed to happen if we let the action pass through a reducer. So that means that dispatch()
should call a reducer and pass in the action. Given how we used reducers in a previous section we have more of an idea now of how to do this. Let's use the reducer function we have already created as well:
function reducer(state = [], action) {
switch(action.type) {
case '[Product] add':
return [...state, action.payload]
default: return state;
}
}
class Store {
constructor() {
this.state = [];
}
dispatch(action) {
this.state = reducer(this.state, action);
}
}
Ok, from the above code we can see that we instantiate our state in the constructor and we can also see that we invoke the reducer()
function in our dispatch()
method and that we do this to compute a new state. Ok, let's take this for a spin:
const store = new Store();
store.dispatch({ type: '[Product] add', { name: 'movie' } });
// store.state = [{ name: 'movie' }]
Supporting more message types
Ok, that’s all well and good but what if we want our state to support more things than a list? Let’s think about this for a second, what do we want our state to look like in our app? Most likely we want it to contain a bunch of different properties, all with their own values, so it makes sense to put all these properties in an object, like so:
{
products: [],
language: 'en'
}
Ok, our current Store implementation clearly doesn't support this, so we need to change it a bit, so let's change it to this:
function reducer(state = [], action) {
switch(action.type) {
case '[Product] add':
return [...state, action.payload]
default:
return state;
}
}
class Store {
constructor() {
this.state = {
products: []
};
}
dispatch(action) {
this.state = {
products : reducer(this.state.products, action)
};
}
}
Realizing we want to store our state as an object we do the necessary changes in the constructor:
this.state = {
products: []
}
This also means our dispatch() method needs to change to:
dispatch(action) {
this.state = {
products : reducer(this.state.products, action)
};
}
This allows our reducer to only be applied to part of the state, namely this.state.products.
Adding one more state property
At this point, we realize that we need to support adding the property language, so we add language to the initial state in the constructor like so:
this.state = {
products: [],
language : ''
};
Ok, so what do we do about the reducer()
, function then? Well at this point we realize we are missing a reducer that should be focused on setting a language, so let's start sketching on that:
function languageReducer(state = '', action) {
switch(action.type) {
case '[Language] load':
return action.payload;
default:
return state;
}
}
let state = languageReducer({
type: '[Language] load',
payload: 'en'
});
Now we have a reducer that is able to set a language. Let’s go back to our Store implementation and add the necessary change to the dispatch()
method:
dispatch(action) {
return {
products : reducer(this.state.products, action),
language: languageReducer(this.state.language, action)
};
}
Let’s also rename reducer()
to productsReducer()
and our full implementation should now look like this:
function productsReducer(state = [], action) {
switch(action.type) {
case '[Product] add':
return [...state, action.payload]
default:
return state;
}
}
function languageReducer(state = '', action) {
switch(action.type) {
case '[Language] load':
return action.payload;
default:
return state;
}
}
class Store {
constructor() {
this.state = {
products: []
};
}
dispatch(action) {
return {
products : productsReducer(this.state.products, action),
language: languageReducer(this.state.language, action)
};
}
}
Handling subscriptions and broadcasts
We have one important aspect left before our implementation is complete. The main things we need to support is, to be able to communicate changes are:
- Sending messages so that state changes
- Set up/tear down subscriptions
- Communicate a change to listeners
We have done the first one so lets support the second one. Let’s implement a subscribe()
and unsubscribe method:
subscribe(listener) {
this.listeners.push(listener);
}
unsubscribe(listener) {
this.listeners = this.listeners.filter(l => l !== listener);
}
2) and 3) are very tightly connected so let’s revisit our dispatch()
method and let's make a change to it so it now looks like this:
dispatch(action) {
this.state = {
products : reducer(this.state.products, action),
language: languageReducer(this.state.language, action)
};
this.listeners.forEach(l => l());
}
Slice of state
This is not a must have but sure is nice. Currently, our state consists of the entire object but let's think for a second how this would be used. It’s likely that the component using this will only be interested in parts of the state, so how do we do that? One way of solving that is to add a select()
method that has the ability to select the part of the state that it wants. It could look like so:
select(fn) {
return fn(this.state);
}
That doesn’t look like much, does it actually work, well let’s look at a use case:
select(state => state.products) select(state => state.language)
Full implementation
Ok, our full code now reads:
// store.js
function productsReducer(state = [], action) {
switch (action.type) {
case '[Product] add':
return [...state, action.payload]
default:
return state;
}
}
function languageReducer(state = '', action) {
switch (action.type) {
case '[Language] load':
return action.payload;
default:
return state;
}
}
class Store {
constructor() {
this.listeners = [];
this.state = {
products: []
};
}
dispatch(action) {
this.state = {
products: productsReducer(this.state.products, action),
language: languageReducer(this.state.language, action)
};
this.listeners.forEach(l => l());
}
subscribe(listener) {
this.listeners.push(listener);
}
unsubscribe(listener) {
this.listeners = this.listeners.filter(l => l !== listener);
}
select(fn) {
return fn(this.state);
}
}
const store = new Store();
module.exports = store;
Using our implementation
Ok, we think we have an implementation that we can use, so let’s apply it to some components:
// components.js
const store = require('./store');
class LanguageComponent {
constructor() {
store.subscribe(this.onChange.bind(this));
this.language = store.select(state => state.language);
}
onChange() {
this.language = store.select(state => state.language);
}
}
class Component {
changeLanguage(newLanguage) {
store.dispatch({ type: '[Language] load', payload: 'en' });
}
}
class ProductsComponent {
constructor() {
store.subscribe(this.onChange.bind(this));
this.products = store.select(state => state.products);
}
add(product) {
store.dispatch({
type: '[Product] add',
payload: product
});
}
onChange() {
this.products = store.select(state => state.products);
}
}
module.exports = {
LanguageComponent,
Component,
ProductsComponent
}
Above we can see we are declaring three components:
-
Component, the purpose of this component is to be the receiver of a user request. The idea is that if a user selects a droplist, containing a list of languages to choose from that should invoke the method
changeLanguage()
on the Component - LanguageComponent, this component is interested in displaying the current language. To know what the current language is, it reads that from the state and it also subscribed to any change event on the store
-
ProductsComponent, this component supports two things, being able to show a list of products but also be able to add items to the product list through the method
add()
Now we just need to create a file app.js
where we can instantiate our components and try invoking some methods to ensure our Redux implementation is working.
Ok, let’s try to invoke the above:
// app.js
const {
LanguageComponent,
ProductsComponent,
Component
} = require('./components');
const store = require('./store');
const component = new Component();
const languageComponent = new LanguageComponent();
const productsComponent = new ProductsComponent();
component.changeLanguage('en');
console.log('lang comp', languageComponent.language);
productsComponent.add({ name: 'movie' });
console.log('products comp', productsComponent.products); console.log('store products', store.state.products);
Summary
Ok, we managed to explain all the core concepts and even managed to create a vanilla implementation of Redux and even show how we would use it with components. You can use this solution for any framework or library. Hopefully, that point has come across that Redux is Pub-Sub but that state is something we care deeply for and we care that the state is changed in an orderly and pure way.
In our next part, we will look into NGRx itself and how to use the Store library.
Top comments (1)
that’s very comprehensive explanation of Redux (along with the first post)! thank you very much!