Skip to content

Instantly share code, notes, and snippets.

@RichardBray
Last active August 27, 2018 05:47
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save RichardBray/6b8377cde3606fb55ce92e7011026909 to your computer and use it in GitHub Desktop.
Save RichardBray/6b8377cde3606fb55ce92e7011026909 to your computer and use it in GitHub Desktop.

A Beginner's Guide to Ngrx/store

As someone who is fairly new to the world of Redux and to a certain extent Angular (not AngularJS), the thought of having to implement Redux to our app was very daunting. Not because it wasn't the right thing to do, our initial app was built quite quickly without much knowledge of Angular and in doing so it wasn't very testable, so Redux would help with that situation, the main difficulty was the learning curve.

Reducers; actions, states, the store, this vernacular was familiar to someone who had experience with Redux but not me, and the whole process is quite verbose, I mean–why would you split your code which was originally in one file, the component into multiple, the reducer, action and state? There are however, a lot of companies who use and find a lot of benefits form it, so I decided to do a deep dive into how and why it works and documented my findings here. This article / tutorial / explanation is ideal for someone who is just getting started in the world of JS frameworks, there are a lot of moving parts when it comes to Redux and I hope this can make sense of most if not all of them.

How Redux works

The Ngrx/store is inspired by Redux and I'll give a little bit of background on what that is before getting started. Redux works on the concept of reducers or more specifically, the reduce method for arrays in Javascript. W3C Schools has a good, simple explanation of it. In short–the reduces arrays into a single value.

reduce()

This code snippet below adds up all the numbers in the array using the getSum function.e

let numbers = [1, 2, 3, 4];

function getSum(total, num) {
    return total + num;
}

console.log(numbers.reduce(getSum)); // result = 10

On the third line the properties total takes the first number, and num the second. It adds both numbers which becomes the new total, then adds the next number in the array going from left to right. So it first is 1 + 2, then 3 + 3, then 6 + 4 to give the result of 10.

Instead of an array of values you can have an array of actions as seen in the example below.

let actions = ['INCREMENT', 'INCREMENT', 'DECREMENT'];

function counter(total, action) {
  switch(action) {
    case 'INCREMENT':
      return total + 1;
    case 'DECREMENT':
      return total -1;
  };
}

console.log(actions.reduce(counter, 0)); // result = 1

This concept can be quite powerful, and be used to run a string of actions in a small amount of code. However it's highly unlikely for modern apps to be this simple, they will most likely have 1000s of lines of logic, and the disadvantages of the native JS .reduce() method is first of all it doesn't run asynchronous, so you can't have streams of them taking place, secondly it gives you the final value at the end without giving you the process of how it got the value which makes it slightly more difficult to debug.

RxJS to the rescue

The Reactive Extensions library for Javascript or simply, RxJS comes built into Angular and solves the problems with .reduce(). It's an asynchronous library which helps with really complex logic for an app, as well as incorporating all the array methods that are in Javascript into it's Observables (they're like arrays but different), RxJS comes with it's own, for our purposes, the .scan() method. This video by Academind does a good job of explaining the difference between .scan() and .reduce() but in essence, scan allows you to see the steps it took to get to a result which is very handy for fixing issues.

I'm not completely aware of the ins and outs but essentially Redux takes advantage of this feature and allows developers to pass each interaction made by the user through which allows for consistent behaviours for components, easy testability, a time travelling debugger, and a bunch of other buzzwords I can't remember right now.

Why use Ngrx over ng2-redux (or angular-redux)?

Here's a quick aside on why at Stratajet we went with Ngrx instead if it's competitor. From the little research I have done ng2-redux (or angular-redux) is a carbon copy of the Redux that is used for react which makes it easier for Reaact devs to jump straight into, but might cause a problem with speed as it isn't completely optimised for Angular. Ngrx on the other hand, was built from the ground up for Angular making it more performant.

How to implement the Ngrx/store into your code

I have put together a very simple tutorial which you can follow along to help you get a brief intro into how to apply this to your Angular app. The code we will use is based off this tutorial for a basic Todo App slightly tweaked to get rid of the tests, styles, and add a 'done' section. We will use syntax for version 4.x of the Ngrx Platform and not 2.x (or any future version).

I will be using the words function and method interchangeably thought the article, for the purpose of this post they both mean the same thing.

1. Clone the todo-app-angular repo

https://github.com/RichardBray/todo-app-angular

Make sure you have the angular cli installed;

$ npm install -g angular-cli

Then run the server with;

$ npm start

Then navigate to loclhost:4200 to see the app. Be sure to play around with it, inspect the code a bit and see how it works.

As you'll notice, most of the logic is in the todo-data.service.ts file which makes it easier for testing which is connected to app.component.ts. The app has three main functions, add a todo item, delete one, and complete one. Let's start by incorporating Ngrx just into the addition of a todo item.

2. Set up Ngrx/store

Add @ngrx/store to the app;

$ npm install @ngrx/store --save

Make a new folder in the /app directory and call it redux and create a file in it called reducer.ts. This reducer file will contain all the logic as pure functions meaning it won't mutate the original data from the store, it will make a copy of it and tweak it accordingly. In that file write the following code;

// todo-app-angular/src/app/redux/reducer.ts
export const todos = (state = [], action) => {

}

Here we've exported a constant function called todos which will take two arguments, the state, and an action. The is the state of the app on launch, in our case as we expect no entries in the todo list we pass it an empty array, otherwise there will be data from a json file or an api. Action is an object with 2 parameters, type and payload. Add the following code to the function.

export const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload];
    default:
      return state;
  }
}

Like the counter example in the Redux explanation above here we are using a switch statement to determine what logic to execute based on the action type. If there's no action it will return the current state. When the 'ADD_TODO' action happens we're returning a new array;

return [...state, action.payload];

Let me explain what this line does. So the three dots ... are called the spread operator, what this does is gets all the content in an array which is state for our case, and makes it editable. As you can see, it's adding action.payload to whatever is currently in the state array.

Let's leave the reducer.ts file for now and go to app.module.ts. Import StoreModule from @ngrx/store and our newly created function, todos from /redux/reducer;

// todo-app-angular/src/app/app.module.ts
import { StoreModule } from '@ngrx/store';
import { todos } from './redux/reducer';

Then add StoreModule to the imports;

imports: [
  BrowserModule,
  FormsModule,
  StoreModule.forRoot({ todos })
],
Note: You might encounter this error after quitting and restarting the server again; *ERROR in Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function (position 2:20 in the original .ts file)...*

Saving any file to automatically restart the server should fix it, I haven't yet found a permanent solution 😞.

In version 2x of @ngrx/store used the .providStore method but for version 4x we use .forRoot and add our todos reducer to it. This makes todos available to the app. Okay let's test this by replacing it with our logic.

3. Finishing the ADD_TODO action

Open the todo-data.service.ts and lets import Store from @ngrx/store.

// todo-app-angular/src/app/todo-data.service.ts
import { Store } from '@ngrx/store';

Then add replace the current constructor with this;

constructor(private _store: Store<any>) {
  _store.select('todos').subscribe(todos => {
    this.todos = todos;
  });
}

The constructor is how dependency injection work in Angular and it's made easier with Typescript. We add a private attribute _store (private attributes have underscores), pass Store<any> to it. Generic types <> are required with the store, for our purposes we will use any, but it's common practice to create an interface called 'IAppState' with your app's structure.

_store.select('todos').subscribe(todos => {
  this.todos = todos;
});

Next we select 'todos' from the store and subscribe to it. (.subscribe() is a method that is provided by RxJS), then we assign the todos from the store to our local todos variable defined on line 8 as an empty array. Phew, hopefully that wasn't too much to take in, now lets look at actually updating the addTodo method.

At the moment, addTodo from line 12 to 18 is firstly checking if there is incrementing from 0 if a todo item has no id, the pushing what the user has typed to the todos array. Let's get rid of all that logic and it's type of TodoDataService.

public addTodo(todo: Todo) {
}

Okay so we need to pass new todos into the _store we defined above and add it to our empty state array from our reducer.ts so it's easily accessible by our app.component.ts file and in turn our app.component.html view. To do that we'll use the .dispatch() method and define a type and payload for our action.

public addTodo(todo: Todo): void {
  this._store.dispatch({
    type: 'ADD_TODO',
    payload: {
      id: ++this.lastId,
      title: todo.title,
      complete: todo.complete
    }
  });
}

As you can see, we've added a type of void since this method isn't returning anything. Then we use dispatch to create an action which will link to our reducer file so we define a type with the same name and a payload of the new todo item. If the type was called something different from what's in the reducer it won't work.

With all this done if you fire up the server and add a new item to the todo list, it should work 👍. Now let's replace our other bits of logic with actions.

4. Creating the other actions

Open the reducer.ts file and add the following code;

// todo-app-angular/src/app/redux/reducer.ts
export const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload];
    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.payload.id);
    case 'TOGGLE_COMPLETE':
      return state.map(todo => {
        if (todo.id === action.payload.id) {
          return Object.assign({}, todo, {
            complete: !todo.complete
          });
        }
        return todo;
      });
    default:
      return state;
  }
};

As you can see we've added two new action types, 'REMOVE_TODO' and 'TOGGLE_COMPLETE'.

case 'REMOVE_TODO':
  return state.filter(todo => todo.id !== action.payload.id);

'REMOVE_TODO' runs the filter() method which applies a function to each item in an array and doesn't display an item if it doesn't comply with the condition. Our condition is true (so leave the item alone) if the todo id does not match the payload id, if it does, filter it out.

case 'TOGGLE_COMPLETE':
  return state.map(todo => {
    if (todo.id === action.payload.id) {
      return Object.assign({}, todo, {
        complete: !todo.complete
      });
    }
    return todo;
  });

'TOGGLE_COMPLETE' is slightly more complicated. The map() method is similar to filter as it applies a function to each array item, but it doesn't filter anything out. First we're checking each todo item to see if it's id matches the payload one, if it does, we return an Object.assign method to copy the todo item and change the complete identifier to the opposite of what it currently is. So if it's true make it false, and vice versa. Then return the new todo. This action type won't work unless return is used all 3 times (I found out the hard way).

Both new action types only require the action.payload.id, so lets pass it to them from the todo-data.service.ts file. Replace the old deleteTodoById and toggleTodoComplete methods with this;

// todo-app-angular/src/app/todo-data.service.ts
public deleteTodoById(todoId: number): void {
  this._store.dispatch({
    type: 'REMOVE_TODO',
    payload: { id: todoId }
  });
}

public toggleTodoComplete(todoId: number): void {
  this._store.dispatch({
    type: 'TOGGLE_COMPLETE',
    payload: { id: todoId }
  });
}

And now let's change the toggleTodoComplete and removeTodo in the app.component.ts to only pass the id to the toggleTodoComplete method instead of the whole todo item. Replace the two old methods with this new code.

// todo-app-angular/src/app/app.component.ts
public toggleTodoComplete({ id }): void {
  this.todoDataService.toggleTodoComplete(id);
}

public removeTodo({ id }): void {
  this.todoDataService.deleteTodoById(id);
}

And that's it. Congratulations you've just made a regular old Angular app work with @ngrx/store.

4. Some optional cleanup

The original app was doing a lot of clever things to replicate what @ngrx/already does so we can get rid of them now. You can get rid of the constructor in the Todo class of the todo.ts file, since our reducer is already doing what it does.

// todo-app-angular/src/app/todo.ts
export class Todo {
  id: number;
  title: string = '';
  complete: boolean = false;

  // constructor(values: Object = {}) {
  //   Object.assign(this, values);
  // }  
  // Don't need ^^
}

You don't need the private functions _updateTodoById and _getTodoById from todo-data.service.ts as they are only used in the toggleTodoComplete method which has been recreated in the reducer.

// todo-app-angular/src/app/todo-data.service.ts
public getIncompleteTodos(): Todo[] {
  return this.todos.filter(todo => todo.complete === false);
}

// Simulate PUT /todos/:id
// private _updateTodoById(id: number, values: Object = {}): Todo {
//   let todo = this._getTodoById(id);
//   if (!todo) {
//     return null;
//   }
//   Object.assign(todo, values);
//   return todo;
// }
//
// Simulate GET /todos/:id
// private _getTodoById(id: number): Todo {
//   return this.todos.filter(todo => todo.id === id).pop();
// }
// Don't need ^^

And you don't need to create a new todo at the end of the addTodo method in the app.component.ts file as once again, the reducer takes care of that.

// todo-app-angular/src/app/app.component.ts
public addTodo(): void {
  this.todoDataService.addTodo(this.newTodo);
  // this.newTodo = new Todo();
  // Don't need ^^
}

Install @ngrx/store-devtools for a visual representation of the store

If you're like me you prefer to see things in action than to visualise them, and it's all well and good saying that your data is in the store but it's easier to understand that if you can see it, so let's add @ngrx/store-devtools to our app.

Let's start by installing the npm;

$ npm install @ngrx/store-devtools --save

Then add the following code to the app.module.ts file.

// todo-app-angular/src/app/app.module.ts
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

Then also update the imports;

imports: [
  BrowserModule,
  FormsModule,
  StoreModule.forRoot({ todos }),
  StoreDevtoolsModule.instrument({
    maxAge: 25 //  Retains last 25 states
  })
]

And last but not least, download the Redux DevTools Chrome App and restart the server.

Conclusion

I hope you found this super useful and you'll find messing around with @ngrx or redux as a whole less intimidating. I'm planning to write a follow-up post on how this can help with testing so follow me on the socials for updates, or if you have any feedback or corrections on this post.

@sajithmx
Copy link

sajithmx commented Dec 6, 2017

Nice article, simplest explanation to ngrx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment