For your Angular

app

Maxim Salnikov

@webmaxru

Reactive State Management

How to build fast, robust, predictable data-driven apps

And enjoy doing this

Maxim Salnikov

  • Google Developer Expert in Angular

  • Mobile Oslo / Angular Oslo / PWA Oslo meetups organizer

  • ngVikings conference organizer

Products from the future

UI Engineer at ForgeRock

ngVikings.org

  • All sides of Angular ecosystem

  • 100% community-driven event from developers for developers

  • True Nordics spirit and Vikings power

Follow Twitter

March 1-2, Helsinki, Finland

Why?

3

3

Increasing complexity

Model

Model

View

View

View

Unidirectional

Action

Dispatcher

Store

View

Action

Maintains

app state

State complexity

  • Server responses

  • Cached data

  • Real-time data feeds

  • Locally created data

  • UI state

3

Redux

Redux is simple

slim-redux.js: 99-liner

Flow

Action

Store

View

Reducer

interaction

dispatch action

new state

state, action

current state

State

A plain object without setters representing entire state of an app

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

Action

A plain object that represents an intention to change the state. Actions are the only way to get data into the store.

{ type: 'ADD_MESSAGE', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

Reducer

A pure function that accepts a state and an action and returns a new state.

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map(
        (todo, index) =>
          action.index === index
            ? { text: todo.text, completed: !todo.completed }
            : todo
      )
    default:
      return state
  }
}

Main principles

Single source of truth

 

1.

Main principles

Single source of truth

 

State is read-only

 

1.

2.

Main principles

Changes are made with pure functions

 

Single source of truth

 

State is read-only

 

1.

2.

3.

Immutability?

Debugging

Performance

Coherence

1.

2.

3.

What about

?

reactjs / redux

angular-redux / store

ngrx / store

=

+

=

Why ngrx/store?

Use AsyncPipe in the templates

Staying consistent with the Angular architecture

Applying reactive middleware

1.

2.

3.

Demo app

MessageStatus

MessageSection

MessageList

MessageForm

Header

App

Demo app

MessageToolbar

MessageStatus

MessageSection

MessageList

MessageForm

Header

App

Demo app

MessageToolbar

MessageStatus

MessageSection

MessageList

MessageForm

Header

App

Demo app

MessageToolbar

MessageStatus

MessageList

MessageForm

App

State Management Container

MessageToolbar

State

State

export interface State {
  messages: MessageReducer.State,
  ui: UiReducer.State;
}
export interface State {
  items: Message[],
  searchQuery: string
}

const initialState: State = {
  items: [],
  searchQuery: 'ngrx'
};
export interface State {
  testPanelOpen: boolean
}

const initialState: State = {
  testPanelOpen: false
};

Actions

import { Action } from '@ngrx/store';
export const CLOSE_TEST_PANEL = '[UI] Close Test Panel';
export const OPEN_TEST_PANEL = '[UI] Open Test Panel';
export class CloseTestPanel implements Action {
    readonly type = CLOSE_TEST_PANEL;
}
...
export type All = CloseTestPanel | OpenTestPanel;

Action with payload

export class Add implements Action {
    readonly type = ADD;

    constructor(public payload: Message) { }
}
export class SearchSuccess implements Action {
    readonly type = SEARCH_SUCCESS;

    constructor(public payload: Message[]) { }
}

Reducer

export function reducer(state = initialState, action: action): State {
  switch(action.type) {
    case MessageActions.ADD: {
      return ...
    }

    case MessageActions.SEARCH: {
      return ...
    }

    case MessageActions.SEARCH_SUCCESS: {
      return ...
    }

    default: {
      return state;
    }
  }
}

Reducer

case MessageActions.SEARCH_SUCCESS: {
  return {
    ...state,
    items: state.items.concat(action.payload)
  };
}

Only non-mutating methods!

  • copyWithin
  • fill
  • pop
  • push
  • reverse
  • shift
  • sort
  • splice
  • unshift
  • ...
  • concat
  • slice
  • filter
  • map

VS

Wiring up all together

export const ROOT_REDUCER = {
  messages: MessageReducer.reducer,
  ui: UiReducer.reducer
};
import { StoreModule } from '@ngrx/store';
import { ROOT_REDUCER } from './states/reducers';
...
@NgModule({
  imports: [
    StoreModule.provideStore(ROOT_REDUCER),
    ...
  ],
  ...
})

Injecting the store

import { Store } from '@ngrx/store';
import * as ApplicationStore from './../states/reducers';
...

export class MessageStatusComponent implements OnInit {

  messagesNumber$: Observable<number>

  constructor(private store: Store<ApplicationStore.State>) {
    this.messagesNumber$ = 
        this.store.select(store => store.messages.items.length);
  }

}
{{ messagesNumber$ | async }}

Creating selectors

this.store.select(store => store.messages.items.length)
this.store.select(ApplicationStore.selectMessagesNumber)
export function selectMessagesNumber(state: State) {
  return state.messages.items.length
}

Dispatching an action

this.store.dispatch({
    type: MessageActions.ADD,
    payload: {
        author: messageForm.value.author,
        text: messageForm.value.text,
        createdAt: new Date()
    }
});

Async action

searchMessages(searchString: string) {

  this.store.dispatch( ...SEARCH... );

  this.messageService.searchMessages(searchString)
    .subscribe(
    messages => {
      this.store.dispatch( ...SEARCH_SUCCESS... );
    });
}
constructor(private messageService: MessageService) { }

ngrx / effects

Isolating side-effects from components

All interaction with outside world: HTTP, WebSockets,

Storage, etc

Centralizing all asynchronous work

1.

2.

3.

Creating effect

@Injectable()
export class MessageEffects {
  @Effect()
  search$: Observable<Action> = this.actions$.ofType(MessageActions.SEARCH)
    .map((action: MessageActions.Search) => action.payload)
    .switchMap(searchQuery => this.messageService.searchMessages(searchQuery))
    .map(results => new MessageActions.SearchSuccess(results));
    
  constructor(private actions$: Actions, private messageService: MessageService)
  {}
}
import { Effect, Actions } from '@ngrx/effects';

Actions for effects

{ type: 'SEARCH' }
{ type: 'SEARCH_SUCCESS', items: { ... } }
{ type: 'SEARCH_FAILURE', error: 'Oops' }

Action

Effect

Action Success

Action Failure

Running effect

import { EffectsModule } from '@ngrx/effects';
import { MessageEffects } from './states/message.effects';
...
@NgModule({
  imports: [
    EffectsModule.run(MessageEffects),
    ...
  ],
  ...
})
searchMessages(searchString: string) {
  this.store.dispatch({
    type: MessageActions.SEARCH,
    payload: searchString
  });
}

Redux DevTools

Live debugging & much more...

Time Traveling (really!)

Save and Restore state

1.

2.

3.

Redux DevTools

import { StoreDevtoolsModule } from '@ngrx/store-devtools';
...
@NgModule({
  imports: [
    storeDevtoolsModule.instrumentOnlyWithExtension({
      maxAge: 5
    }),
    ...
  ],
  ...
})

Redux DevTools

Out of today's scope

Many other ngrx features. Stay tuned!

ngrx/router-store to connect with Angular router

Composing Reducers to apply Middleware

1.

2.

3.

Tips & Tricks

Use ngrx-store-freeze to ensure

immutability during development

Use reactjs/reselect to create efficient Selectors

Use CLI's environment feature to conditionally wire up store/devtools

1.

2.

3.

To sum up

More boilerplate code and some

architectural limitations

Great performance

Awesome developer experience

1.

2.

3.

The decision is yours!

תודה רבה לך

@webmaxru

Maxim Salnikov

Questions?

Reactive State Management For Your Angular App

By Maxim Salnikov

Reactive State Management For Your Angular App

Your Angular application is a reactive system. It reacts to different events and updates the model, then propagates the changes through the component tree. It works like a charm for the simple and non UI-intensive apps. But when you, following this architecture, meet some more complex usecases (like concurrent data modification, complex component intercommunications, the need to keep temporary UI state, etc), it's time to think about different way to manage the app state. Redux pattern to the rescue! But Redux in Angular way. We have reactive forms, reactive router, observables-based http-client, so let's have a look at reactive Redux called ngrx/store. In this session: intro to the state management (famous Facebook bug), base principles and components of Redux, converting our regular app to the one using centralized store using ngrx/store, tooling, pros and cons of having centralized store for the state management.

  • 4,291