If you are a software developer and aren't in another world, you have probably read or heard something about Redux. Today Redux is very popular, but not as much as some years ago. The first time that I heard about React, the second word was always Redux, "How to init with react and redux", "Todo app with react redux", and another hundred names. Even though Redux is not as popular today as in the past, I think it's very helpful that you know how Redux works and how you can create a simple version of this famous pattern.
Redux is a pattern that provides a way to manage your application state. Redux pattern increased in popularity when front-end applications became more complex, and when we started to develop applications with many components, sharing the same state. You can find a lot of libraries that implement this, we have Redux for React and NGXS and NgRx for Angular.
Since Redux is very popular I assume that you know how it works and how to use it, but I will provide some basic information about it. If you want to know more, however, I recommend that you read the documentation about the patterns and how some libraries do this.
Redux explanations
-
Actions - basically actions are like instructions that drive your intentions, you need a
type
that describes what you want to do, and if you need some data to perform your intentions, it's necessary to provide apayload
.
const action = { type:'ADD_TODO', payload: {label:'Push up', complete:false} }
-
Reducers - reducers are pure functions that perform your intentions in the application state, that function receives the current state and the action, that is running. For every action, you should provide a reaction in your reducer.
function reducer(state, action){ switch(action.type){ case 'ADD_TODO': // do something case 'DELETE_TODO': // do another thing } }
Store - I like to think about Store as a place where you have the real state, and it provides the resources to get the state
store.getState()
, and register listeners.
Our Redux API
This is how we will initiate our Store.
const store = new Store(reducers, initialState)
Our redux implementation has some public methods and private method:
- dispatch() - This method will receive the instruction that will drive how the state will be changed.
- subscriber() - With this method, we can subscribe listeners to know when the application state is changed. These listeners will be simple functions that can receive the new state changed as argument.
- value() - These methods will return the current state of the application.
class Store{
constructor(){}
dispatch()
subscriber()
}
We need another method to perform the state changes, reducer
is the last method that we need in our implementation.
class Store{
//...
private reduce()
}
Okay, we need more than methods in our redux implementation, we need a property to hold the state application, state
and another to hold the reducers application and one more to hold the subscribers.
class Store{
private state:{[key:string]:any}
private reducers:{[key:string]:Function}
private subscribers: Array<Function>;
//...
}
Implementation
Constructor
Let's go ahead. You may notice that I'm using typescript, but feel free to use javascript. Our first step is to write the constructor method, constructor
needs to receive the reducers and the initialState, so let's do that:
class Store{
//...
constructor(reducers={}, initialState={}){
this.reducers = reducers
this.state = initialState
}
//...
}
Here we assign reducers
and initialState
to state
and reducers
properties.
Using it:
const reducers = {
todoReducer:(state, action) => ({...})
}
const initialState = {
todoReducer:{todos:[]}
}
const store = new Store(reducers, initialState)
Reduce
As I mentioned earlier, reduce will perform and return the state changed.
class Store{
//...
private reduce(state, action) {
const newState = {}
for (const prop in this.reducers) {
newState[prop] = this.reducers[prop](state[prop], action)
}
return newState
}
}
Here we iterate reducers registered in Store and invoke every reducer passing as argument the current state and the current action. After that, we save the result returned by every reducer in the correct state property. Finally, we return to the new state. Since the reduce method is a private method it will not be available to use out of class.
Subscribe
Subscribe will allow us to have many state change listeners, so let's implement it.
class Store{
//...
subscribe(fn:Function){
this.subscribers = [...this.subscribers, fn];
return () => {
thi.subscribers = this.subscribers.filter(subscriber => subscriber !== fn)
}
}
}
Here we received a function that will be invoked when some changes happen in the state, subscriber
will add the fn
argument into subscribers
property. The last part of this method will return another function that when invoked will remove the fn
function passed as argument. The function that will be returned knows the subscriber
method context, for this reason, we can compare fn
argument with every subscriber registered in our Store and decide who needs to be removed.
Using it:
//...
const store = new Store(reducers, initialState)
function callback(state){
// do something
}
const unsubscribe = store.subscribe(callback)
unsubscribe()// wii remove callback function
Dispatch
Let's implement this method and learn how it works.
class Store{
//...
dispatch(action) {
this.state = this.reduce(this.state, action)
this.subscribers.forEach(fn => fn(this.state))
}
}
When reduce
method is invoked it returns a new state and assigns it to state
property of the store. After that, we iterate subscribers
property and invoke every subscribed function passing the new state as argument, this way every listener will be notified with the new application state.
Using it:
//...
const store = new Store(reducers, initialState)
const action = {
type:'ADD_TODO',
payload: {label:'Push up', complete:false}
}
store.dispatch(action)
Value
Our last method implementation will be a get method that will return us the current state, let's implement it:
class Store{
//...
get value() {
return this.state;
}
}
Final code
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]
return () => {
this.subscribers = this.subscribers.filter(subscriber => subscriber !== 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
}
}
Finally, we have a simple implementation of redux, this implementation does not reflect any redux library implementation, it's just a possible implementation of redux. The principal goal of this post is to show you a simple way of how redux works. If you are more interested in a functional approach, let me know and we can address it in the next post.
I hope that you enjoyed this post. Stay Safe!!!
Top comments (9)
I think you meant to say NgRx not RXJS.
NgRx is the implementation of the redux pattern for angular using rxjs.
RXjs is the JS implementation of reactive extensions which itself is an implementation of the observable pattern amongst other things.
NgRx has the stores, actions, etc
Rxjs does not have those concepts.
Yeah, I was confused about RXJS and NGXS and NgRx, thanks
Oh I hadn’t even heard of ngxs!
When we think how Angular works, IMO NGXS makes more sense than NgRx
Interesting, why do you think that?
Okay, my opinion is based in old versions of NgRx, but it's strange that we have a function as a reducer in Angular, but with ngxs we hava a class with decorators.
This approach make more sense for me when we are using Angular.
It's like a foreign body.
Why are you using this.value in dispatch and subscribe in the final code?
Thanks for comment, this.value is get method, that method returns the current state. (get and set methods don't need () to be invoked in typescript )
Thanks for the clarification and reminder to avoid classical programming ;-)