NGRX — from the beginning, part II, Redux

Chris Noring - Apr 3 '19 - - Dev Community

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:

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' };
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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' } 
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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; 
 } 
}
Enter fullscreen mode Exit fullscreen mode

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'} 
});
Enter fullscreen mode Exit fullscreen mode

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) {} 
}
Enter fullscreen mode Exit fullscreen mode

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); 
  } 
}
Enter fullscreen mode Exit fullscreen mode

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' }]
Enter fullscreen mode Exit fullscreen mode

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' 
}
Enter fullscreen mode Exit fullscreen mode

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) 
    }; 
  } 
}
Enter fullscreen mode Exit fullscreen mode

Realizing we want to store our state as an object we do the necessary changes in the constructor:

this.state = { 
 products: [] 
}
Enter fullscreen mode Exit fullscreen mode

This also means our dispatch() method needs to change to:

dispatch(action) { 
  this.state = { 
    products : reducer(this.state.products, action) 
  }; 
}
Enter fullscreen mode Exit fullscreen mode

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 : '' 
};
Enter fullscreen mode Exit fullscreen mode

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' 
});
Enter fullscreen mode Exit fullscreen mode

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) 
  }; 
}
Enter fullscreen mode Exit fullscreen mode

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) 
    }; 
  } 
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Sending messages so that state changes
  2. Set up/tear down subscriptions
  3. 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); 
}
Enter fullscreen mode Exit fullscreen mode

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()); 
}
Enter fullscreen mode Exit fullscreen mode

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); 
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player