Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Managing State in Angular 2 Applications

DZone's Guide to

Managing State in Angular 2 Applications

Managing application state is a hard problem. Patterns like Redux and Flux are designed to address this problem by making coordination more explicit. In this article, I will show how we can implement a similar pattern in just a few lines of code using RxJS. Then, I will show how we can use this pattern to implement a simple Angular 2 application.

· Web Dev Zone
Free Resource

Discover how to focus on operators for Reactive Programming and how they are essential to react to data in your application.  Brought to you in partnership with Wakanda

Managing application state is a hard problem. You need to coordinate between multiple backends, web workers, and UI components. Patterns like Redux and Flux are designed to address this problem by making this coordination more explicit. In this article, I will show how we can implement a similar pattern in just a few lines of code using RxJS. Then, I will show how we can use this pattern to implement a simple Angular 2 application.

Core Properties

When talking about an architecture pattern, I like to start with describing its core properties—something that you can write down on the back of a napkin. The devil, of course, is in the details, and we will get to that. But, a high-level overview is useful nevertheless.

In many ways, what we are going to build is akin to Redux.

Immutable State

The whole application state is stored as an immutable data structure. So every time a change happens, a new instance of the data-structure is constructed. Even though this seems limiting at first, this constraint results in many great properties. One of which is that it can make your Angular applications quite a bit faster.

Interaction = Action

The only way to update the application state is to emit an action. As a result, most user interactions emit actions. And, if you want to simulate an interaction, you just need to emit the right sequence of actions.

Application is fn(a:Observable ):Observable

The logic of the application is expressed as a function mapping an observable of actions into an observable of application states.

function stateFn(initState: AppState, actions: Observable<Action>): Observable<AppState> { … }

This function is invoked only once. This is different from Redux, where the reducing function is invoked on every action.

Application and View Boundary

The application and view logic are completely separated. The dispatcher and state objects are the boundary through which the application and the view communicate. The view emits actions using the dispatcher and listens to changes in the state.

Example

Since this pattern is similar to Redux, you may benefit from watching Dan Abramov’s excellent video course on the subject. In this course, Dan shows how to build a todo application using Redux. To make it easier for you to compare the Redux implementation and my implementation, I will build the same app in this article.

You can play with the example I will build in this blog post here: Plunk.

Application State

I get a really good feel of what an application does by looking at its state’s type definitions and its list of actions. So, let’s start with that.

interface Todo { id: number; text: string; completed: boolean; }
interface AppState { todos: Todo[]; visibilityFilter: string; }

Our application’s state is just an array of todos and a filter defining which todos to display.

Actions

Our application will support the following three actions: AddTodoAction, ToggleTodoAction, and SetVisibilityFilter.

class AddTodoAction       { constructor(public todoId: number, public text: string){} }
class ToggleTodoAction    { constructor(public id: number){} }
class SetVisibilityFilter { constructor(public filter: string){} }

type Action = AddTodoAction|ToggleTodoAction|SetVisibilityFilter;

The type Action, which is a union of the three actions, represents everything this application will be able to do.

Observable

We need to define the state function that will return an observable of application states. To make the example a little bit more production-like, I will split the state function into two parts: one dealing with todos and the other one dealing with the visibility filter.

Let’s deal with todos first.

function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
  return actions.scan(initState, (state, action) => {
    if (action instanceof AddTodoAction) {
      const newTodo = {id: action.todoId, text: action.text, completed: false};
      return [...state, newTodo];
    } else {
      return state;
    }
  });
}

There are a few interesting things that I’d like to point out.

First, look at the signature of the function. The function does not take a single action, but rather an RxJS observable of actions. Likewise, it does not return a list of todos, but an observable.

Second, actions.scan applies the accumulator function over an observable sequence and returns each intermediate result. So, a new list of todos will be emitted after every action.

Third, TypeScript realizes that the action inside the if clause is an AddTodoAction. This means that I can access the todoId and text properties, but not filter. This allows me to write such functions in a type-safe way. This is fantastic because such functions are where the smarts of your application live, and as a result, they quickly become non-trivial. So, having some compiler support there is a big plus.

Next, let’s extend todos by adding an ability to toggle a todo.

function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
  return actions.scan((state, action) => {
    if (action instanceof AddTodoAction) {
      const newTodo = {id: action.todoId, text: action.text, completed: false};
      return [...state, newTodo];
    } else {
      return state.map(t => updateTodo(t, action));
    }
  }, initState);
}

function updateTodo(todo: Todo, action: Action): Todo {
  if (action instanceof ToggleTodoAction) {
    // merge creates a new object using the properties of the passed in objects
    return (action.id !== todo.id) ? todo : merge(todo, {completed: !state.completed});

  } else {
    return todo;
  }
}

Similar to todos, we can implement a function creating an observable of visibility filter.

function filter(initState: string, actions: Observable<Action>): Observable<string> {
  return actions.scan((state, action) => {
    if (action instanceof SetVisibilityFilter) {
      return action.filter;
    } else {
      return state;
    }
  }, initState);
}

And, finally, we can combine them to create stateFn.

function stateFn(initState: AppState, actions: Observable<Action>): Observable<AppState> {
  const combine = s => ({todos: s[0], visibilityFilter: s[1]});

  const appStateObs: Observable<AppState> = todos(initState.todos, actions).
    zip(filter(initState.visibilityFilter, actions)).map(combine);

  return wrapIntoBehavior(initState, appStateObs);
}

There is a lot going on in this six lines of code.

First, we create the todos and filter observables using the functions defined above. Then, we zip them into an observable of pairs, which we map into an observable of AppState.

There is one problem with this observable. If a component subscribes to it, the component won’t receive any data until the observable emits a new event. For this to happen, a new action has to be emitted. This is not what we want. What we want is for the component to receive the latest snapshot the moment it subscribes. And, that is what BehaviorSubject is for.

function wrapIntoBehavior(init, obs) {
  const res = new BehaviorSubject(init);
  obs.subscribe(s => res.next(s));
  return res;
}

A behavior subject is an observable that will emit the latest value to every new subscriber.

To better understand how stateFn works, let’s write a few unit tests:

it('should create a new todo', () => {
  const actions = new Subject<Action>();
  const states = stateFn({todos: [], visibilityFilter: 'SHOW_ALL'}, actions);

  actions.next(new AddTodoAction(100, 'todo1'));
  actions.next(new AddTodoAction(101, 'todo2'));

  states.subscribe(s => {
    expect(s.todos.length).toEqual(2);
    expect(s.todos[0]).toEqual({id: 100, text: 'todo1', completed: false});
    expect(s.todos[1]).toEqual({id: 101, text: 'todo2', completed: false});
  });
});

it('should toggle a todo', () => {
  const actions = new Subject<Action>();
  const todos = [
    {id: 100, text: 'todo1', completed: false},
    {id: 101, text: 'todo2', completed: false}];
  const states = stateFn({todos: todos, visibilityFilter: 'SHOW_ALL'}, actions);

  actions.next(new ToggleTodo(100));

  states.subscribe(s => {
    expect(s.todos[0].completed).toBeTrue();
    expect(s.todos[1].completed).toBeFalse();
  });
});

If you are familiar with Redux, you can find the stateFn function to be similar to a Redux reducer. But, there is actually a big difference: the stateFn function is invoked only once, whereas a Redux reducer is invoked on every action.

This is important for the following reasons:

Just an Observable

The stateFn function is called only once to create the state observable. The rest of the application (e.g., Angular 2 components) does not have to know that stateFn even exists. All they care about is the observable. This gives us a lot of flexibility in how we can implement the function. In this example, we did it in a Redux-like way. But, we can change it without affecting anything else in the application. Also, since Angular 2 already ships with RxJS, we did not have to bring in any new libraries.

Synchronous and Asynchronous

In this example, stateFn is synchronous. But, since observables are push-based, we can introduce asynchronicity without changing the public API of the function. So, we can make some action handlers synchronous and some asynchronous without affecting any components. This gets important with the growth of the application, when more and more actions have to be performed in a web-worker or server. One downside of using push-based collections is that they can be awkward to use, but Angular 2 provides a primitive–the async pipe–that helps with that.

Power of RxJS

RxJS comes with a lot of powerful combinators, which enables implementing complex interactions in a simple and declarative way. For instance, when action A gets emitted, the application should wait for action B and then emit a new state. If, however, the action B does not get emitted in five seconds, the application should emit an error state. You can implement this in just a few lines of code using RxJS.

This is key. The complexity of application state management comes from having to coordinate such interactions. A powerful tool like RxJS, which takes care of a lot of coordination logic, can dramatically decrease the complexity of the state management.

Application and View Boundary

At this point, we have not written any Angular-specific code yet—we have not written a single component. This is one of the benefits of this architecture—the application and the view logic are separated. But, how do they communicate?

They communicate via the dispatcher and state objects.

State and Dispatcher

We can create them as follows:

const initState = new OpaqueToken("initState");
const dispatcher = new OpaqueToken("dispatcher");
const state = new OpaqueToken("state");

const stateAndDispatcher = [
  provide(initState, {useValue: {todos: [], visibilityFilter: 'SHOW_ALL'}}),
  provide(dispatcher, {useValue: new Subject<Action>(null)}),
  provide(state, {useFactory: stateFn, deps: [new Inject(initState), new Inject(dispatcher)]})
];
  • dispatcher is an RxJS subject, which means that it is both an observable and an observer. So, we can pass it into stateFn, and use it to emit actions.
  • state is an observable returned by the stateFn function.

We can register the providers like this:

@Component({
  selector: 'todo-app',
  template: `...`,
  providers: stateAndDispatcher
})
class TodoApp {}

And, then inject them into components.

@Component(...)
class FilterLink {
  constructor(@Inject(state) private state: Observable<AppState>){}
}

No Store

Note, that in opposite to Redux or Flux, there is no store. The dispatcher is just an RxJS observer, and state is just an observable. This means that we can use the built-in RxJS combinators to change the behavior of these objects, provide mocks, etc.

No Global Objects

Because we use dependency injection to inject the state and the dispatcher, and those are two separate objects, we can easily decorate them. For instance, we can override the dispatcher provider in a component subtree to log all the emitted actions from that subtree only. Or, we can wrap the dispatcher to automatically scope all actions, which can be very handy when multiple teams are working on the same application. We can also decorate the state provider to, for instance, enable debouncing.

View

Finally, we got to the most interesting part–implementing the view layer.

Displaying Todos

Let’s start with a component rendering a single todo.

@Component({
  selector: 'todo',
  template: `<span (click)="toggle.next()" [style.textDecoration]="textEffect">
               {{text}}
             </span>`
})
class Todo {
  @Input() text: string;
  @Input() completed: boolean;
  @Output() toggle = new EventEmitter();

  get textEffect() { return this.completed ? 'line-through' : 'none'; }
}

This is what Dan Abramov calls a dumb or presentational component. This component is not aware of the application side of things. It only knows how to render a todo.

Next, the todo list component.

@Component({
  selector: 'todo-list',
  template: `<todo *ngFor="#t of filtered|async"
                [text]="t.text" [completed]="t.completed"
                (toggle)="emitToggle(t.id)"></todo>`,
  directives: [Todo]
})
class TodoList {
  constructor(@Inject(dispatcher) private dispatcher: Observer<Action>,
              @Inject(state) private state: Observable<AppState>) {}

  get filtered() { return this.state.map(s => getVisibleTodos(s.todos, s.visibilityFilter)); }

  emitToggle(id) { this.dispatcher.next(new ToggleTodoAction(id)); }
}

The first thing we do here is create an observable of filtered todos using the injected state. Since observables are push-based, they can be awkward to work with. We in the Angular team recognize this, which is why we provide a few things to make it easier. For instance, the async pipe "extracts" the last value of an observable. This allows us to use an observable of an object in any place where that object is required.

Second, we use the injected dispatcher to a emit new action in the emitToggle event handler.

This component is aware of the application because it injects the dispatcher and the state. Some can say this component mixes presentational and non-presentational concerns. So, if you feel strong about it, you can separate this component into two. But, I am not sure if it will buy you that much. Angular components already separate presentational aspects into a template. Splitting every such component into two might be an overkill.

Adding Todos

Next, let’s create a component adding todos.

@Component({
  selector: 'add-todo',
  template: `<input #text><button (click)="addTodo(text.value)">Add Todo</button>`
})
class AddTodo {
  constructor(@Inject(dispatcher) private dispatcher: Observer<Action>) {}
  addTodo(value) { this.dispatcher.next(new AddTodoAction(nextId++, value)); }
}

Filtering Todos

Now, let’s add an ability to filter todos:

@Component({
  selector: 'filter-link',
  template: `<a href="#" (click)="setVisibilityFilter()"
               [style.textDecoration]="textEffect|async"><ng-content/></a>`
})
class FilterLink {
  @Input() filter: string;
  constructor(@Inject(dispatcher) private dispatcher: Observer<Action>,
              @Inject(state) private state: Observable<AppState>){}

  get textEffect() { return this.state.map(s => s.visibilityFilter === this.filter ? 'underline' : 'none'); }

  setVisibilityFilter() { this.dispatcher.next(new SetVisibilityFilter(this.filter)); }
}

@Component({
  selector: 'footer',
  template: `<filter-link filter="SHOW_ALL">All</filter-link>
            <filter-link filter="SHOW_ACTIVE">Active</filter-link>
            <filter-link filter="SHOW_COMPLETED">Completed</filter-link>`,
  directives: [FilterLink]
})
class Footer {}

Root Component

Finally, we add a root component combining different parts into our application.

@Component({
  selector: 'ng-demo',
  template: `
    <add-todo></add-todo>
    <todo-list></todo-list>
    <footer></footer>
  `,
  directives: [AddTodo, TodoList, Footer],
  providers: stateAndDispatcher
})
class TodoApp {}

It is Fast!

The pattern described does not just make Angular applications easier to organize and refactor. It also makes it more performant. This is because when the application state is stored as an immutable data structure, and the changes in the state are represented as an observable, we can set the OnPush strategy for all the Angular components. To learn more about it, read Angular, Immutability, and Ecapsulation.

Other Ways To Manage State

Of course, this is not the only way to manage application state in Angular apps. For instance, you can use @ngrx/store. You can also write applications that do not use immutable data or observables, and instead use use-case services or DCI.

Summary

Coordinating between multiple backends, web workers, and UI components is what makes managing application state such a challenging task. Patterns like Redux and Flux help address this. In this article, I showed how easy it is to implement a similar pattern in just a few lines of code using RxJS. Then, I showed how we can use it to implement a simple Angular 2 application.

Links

Follow Victor on Twitter to learn more.

Code licensed under an MIT-style License.

Learn how divergent branches can appear in your repository and how to better understand why they are called “branches".  Brought to you in partnership with Wakanda

Topics:
angular 2.0 ,web developement

Published at DZone with permission of Victor Savkin, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}