Update:
Even though this post was written in 2016 the concepts still hold. In that time the reselect library has had several new releases, be sure to read their updated API's here.
Ever since we discovered Redux around a year ago, we've been in love with the elegance of Redux-based web applications. By now we've used Redux on countless projects, combining it with React, Angular 1 and Angular 2. In this post we show how it can be used with Angular 2, including some nifty features.
If you are new to Redux, I recommend checking out the official documentation, or watching the exellent Getting Started with Redux Series by Dan Abramov. However, the core concepts of Redux architecture are as follows,
- A global immutable application state
- Unidirectional data-flow
- Changes to state are made in pure functions, or reducers.
Before getting into how to use Redux with Angular 2, first we will take a look at a typical Todo style application to demonstrate actions and reducers.
Redux Actions
Actions in Redux are functions that ultimately return plain JSON objects. They can be very basic like the examples below, in which we just form up objects with the arguments passed in. However, this is also where more business logic and validation can be done.
Actions are also where API calls in your application can happen, such as making a request to create a user, load data, etc. The result from these requests is what eventually will get dispatched to your reducers to handle.
src/actions/todo.ts
import * as types from '../constants'
export function addTodo(text) {
return { type: types.ADD_TODO, text }
}
export function deleteTodo(id) {
return { type: types.DELETE_TODO, id }
}
/// ...
Actions do not live on their own though, and need to be handled by a reducer.
Redux Reducers
Reducers in Redux are where we modify the application state in response to an action being completed. You can think of your reducers being a bit of database for your application, and often the logic in your reducers is simple and concerned with updating the data to reflect the changes in the system.
In a Redux application, the only way to modify the state of your application is to dispatch an action. You are not able to reach into the store directly to modify it. This helps ensure a unidirectional data-flow in our applications, and leads to a more predictable and understandable system.
Here's an example of a reducer for handling a list of Todo items.
src/reducers/todo.ts
import { ADD_TODO, DELETE_TODO } from '../constants'
export interface Todo {
id: number;
completed: boolean;
text: string;
}
export interface TodoState extends Array<Todo> { };
const INITIAL_STATE = [
{
text: 'Use Redux',
completed: false,
id: 0
}
]
const getNextId = (todos) => {
todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1
}
export default function todos(state: TodoState = INITIAL_STATE,
action): TodoState {
switch (action.type) {
case ADD_TODO:
return [
{
id: getNextId(state),
completed: false,
text: action.text
},
...state
]
case DELETE_TODO:
return state.filter(todo =>
todo.id !== action.id
)
default:
return state;
}
}
This reducer is effectively listening to the selected actions fired by the application, and specifies how the state will be changed in response to each action. Note that this is a pure function: it returns a new state each time instead of modifying the existing one.
This example is using Object.assign, and the Spread operator to ensure that new objects and new instances of our collection are returned when we need to make modifications to the state.
Bootstrapping Angular 2 and Redux
Now that we have created an action and a reducer, let's take a look at how to get Angular 2 aware of Redux. For this, we will be using a library called ng2-redux, which is available on npm via npm install ng2-redux.
First, we need to create our Redux store and register the middleware and reducers that we want to use with it.
src/store/configure-store.ts
import { createStore,
applyMiddleware,
compose,
StoreEnhancerStoreCreator } from 'redux';
const thunk = require('redux-thunk').default;
import reducer from '../reducers/index';
const finalCreateStore = compose(
applyMiddleware(thunk)
)(createStore);
export default () => {
return finalCreateStore(reducer);
}
Once we have our store created, we need to register it with an Angular 2 provider. To do this we will import provider from ng2-redux, and tell Angular to use this during the bootstrap phase of our application.
src/index.ts
import { enableProdMode, provide } from 'angular2/core';
import { bootstrap } from 'angular2/platform/browser';
import { ROUTER_PROVIDERS, APP_BASE_HREF } from 'angular2/router';
import configureStore from './store/configure-store';
import { RioSampleApp } from './containers/sample-app';
import { provider } from 'ng2-redux';
const store = configureStore({});
bootstrap(RioSampleApp, [
provider(store),
ROUTER_PROVIDERS,
provide(APP_BASE_HREF, { useValue: '/' })
]);
This will register NgRedux as a provider with Angular 2 allowing it to be injected into our components and services.
Accessing Redux in Components
Now that we've set up our store, actions, and ngRedux, let's see how to access this data in a component.
src/containers/todo-page.ts
// ...
import 'NgRedux' from 'ng2-redux';
import { AsyncPipe } from 'angular2/common';
@Component({
selector: 'todo-page',
pipes: [AsyncPipe],
template: `
<ul>
<li *ngFor="#todo of todos$ | async">{{todo.text}} - {{todo.completed}}
</ul>
`
})
export class RioTodoPage implements OnInit {
todos$: Observable<any>
constructor(private ngRedux: NgRedux) { }
ngOnInit() {
this.todos$ = this.ngRedux.select('todos')
}
}
There is a bit going on here, so lets break this down a bit.
ngRedux.select
First, in the ngOnInit hook we are telling ngRedux that we want to observe the key in our state called todos.
Whenever a change happens in our application state that affects todos, the component will be notified and update as a result. If something has happened in our application that does not affect todos, then nothing will happen.
If you want to do more complex logic or transformations you are also able to provide a function instead of a key in the state.
this.completedTodos$ = this.ngRedux
.select(state=>state.todos.filter(n=>n.completed===true))
Every time a change happens to your application state, the select functions will be executed and compare the previous results to the current results. If the data has changed, the subscribers will be notified of it.
We also have the ability to pass in a custom compare function to .select. This is useful if we know more information about the structure of our data and what should be treated as a change or not. For example, if we had a Person object but we only cared if the firstName property had changed, we could provide a custom compare function like this:
let comparePerson = (a,b) => a.firstName === b.firstName;
this.person$ = this.ngRedux.select(state => state.person, comparePerson)
Once we've used ngRedux.select to set up Observables on our state, let's see how to get that data into our component's template:
<ul>
<li *ngFor="#todo of todos$ | async">{{todo.text}} - {{todo.completed}}
</ul>
Since we are exposing the todos as an observable, we are able to use Angular 2's AsyncPipe both to automatically subscribe for changes, and also to deal with disposing subscriptions once the component is destroyed
Another benefit of exposing your state as an Observable, is that it can be combined with other streams - including other slices of state, events from web-sockets, DOM events, etc. You also have access to all of the operators available to RxJs for combining, transforming and filtering streams.
At this point though, we are not interacting with our state in any way or dispatching actions. Before wiring up the actions let's take a minute to refactor the the todo item into its own component.
Component Refactoring
src/components/todo-item/index.ts
import { Component,
Input,
EventEmitter,
Output,
ChangeDetectionStrategy } from 'angular2/core';
@Component({
selector: 'rio-todo-item',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input className="toggle"
type="checkbox"
[ngModel]="todo.completed"
(ngModelChange)="todoCompleted.emit(todo.id)" />
<input [ngModel]="todo.text"
(ngModelChange)="todoEdited.emit({id: todo.id, text: $event})"/>
<button type="button"
(click)="todoDeleted.emit(todo.id)">X</button>
`
})
export class RioTodoItem {
@Input() todo: any;
@Output() todoCompleted: EventEmitter<any> = new EventEmitter();
@Output() todoEdited: EventEmitter<any> = new EventEmitter();
@Output() todoDeleted: EventEmitter<any> = new EventEmitter();
constructor() { }
};
We are creating a presentational component that accepts a todo item as an input, and will emit events when one is marked as completed, edited or deleted.
The difference between a presentational component, and a container component is that the presentational components do not know anything about redux or the system around it. They accept data as inputs, and emit events as outputs.
The container component is responsible for knowing about Redux, how to connect to the state to fetch data, and how to dispatch actions to modify the state of the application.
This allows the RioTodoItem to be stateless, and it is up to the containers to handle dispatching the events. This allows the component to be reusable, even outside of a Redux application.
Next, lets go back to our RioTodoPage container to make use of this new component, and start hooking up some events. First, lets look at the template for our RioTodoPage component.
<ul>
<li *ngFor="#todo of todos$ | async">
<rio-todo-item [todo]="todo"
(todoCompleted)="completeTodo($event)"
(todoDeleted)="deleteTodo($event)"
(todoEdited)="editTodo($event)">
</rio-todo-item>
</ul>
RioTodoItem will emit events for todoCompleted, todoDeleted, and todoEdited. This component itself does not know how to update the application state, and it's the responsibility of our RioTodoPage to dispatch those actions.
Before looking into the shorthand helper of mapDispatchToTarget, I'll cover using ngRedux.dispatch directly.
ngRedux.dispatch
src/containers/todo-page.ts
// ....
import {
deleteTodo,
editTodo,
completeTodo
} from '../actions/todo';
import { RioTodoItem } from '../components';
@Component({
// ...
})
export class RioTodoPage {
todos$: Observable<Todos>;
constructor(private ngRedux:NgRedux) { }
ngOnInit() {
this.todos$ = this.ngRedux.select('todos');
}
completeTodo(id: number): void {
this.ngRedux.dispatch(completeTodo(id));
}
deleteTodo(id: number): void {
this.ngRedux.dispatch(deleteTodo(id));
}
editTodo({id, text}: Todo): void {
this.ngRedux.dispatch(editTodo(id,text));
}
}
While this does lead to slightly more verbose classes, it also becomes more explicit to what methods are on your class and can lead to less confusion. It can also be preferable to hook up your actions this way if your components are not emitting events that do not match the arguments of your action creators. For example, in our editTodo, the event being emitted is an object with an id and text property. However, our editTodo is expecting these as properties.
However, if you want to cut down on some boilerplate code, we can use the ngRedux.mapDispatchToTarget.
ngRedux.mapDispatchToTarget
What mapDispatchToTarget to target does, is take in an object that contains the actions you want to use, wraps them in the Redux dispatch and maps the result onto your component instance. For example, the above component could be changed to:
export class RioTodoPage {
todos$: Observable<Todos>;
completeTodo: (id:number)=>void;
deleteTodo: (id:number)=>void;
constructor(private ngRedux:NgRedux) { }
ngOnInit() {
this.todos$ = this.ngRedux.select('todos');
this.ngRedux.mapDispatchToTarget({
completeTodo,
deleteTodo})(this)
}
editTodo({id, text}) {
this.ngRedux.dispatch(editTodo(id,text));
}
}
More to come
This is just scratching the surface ng2-redux and we have plans to continue enhancing the library to make working with Redux and Angular 2 even easier. Some of the features on the roadmap include property and method decorators to help reduce boiler plate, for example:
import { complexStateSelector } from '../selectors';
import { addTodo, editTodo } from '../actions'
export class MyComponent {
@select(state=>state.todos) todos$;
@select() session$;
@select(complexStateSelector) data$;
@dispatch(addTodo) addTodo: ()=> any;
}
While these features are not available yet, they are on our roadmap and will keep you posted on when they become available, and how to use them.