Angular

NgRx

Redux

MVC Pattern

Controller

User

Model

View

Updates

Manipulates

Sees

Uses

Dependency hell

Model

Model

View

View

View

Flux

Data flow

Action

Store

Dispatcher

View

Popular flux
implementations

  • Redux
  • Reflux
  • Fluxxor
  • Flummox
  • Alt

Redux

Key principles

  • Single Source Of Truth
  • State is Readonly
  • Changes are made with pure functions

Single Source Of Truth

The state of your whole application is stored in an object tree within a single store.

1
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

State is Readonly

The only way to change the state is to emit an action - special object that describes the way of changing the state. 

1
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}
const ADD_TODO = 'ADD_TODO'
store.dispatch(addTodo('Learn about actions'));

Changes are made with pure
functions

To specify how the state tree is transformed under specific actions, you need a pure function called reducer

1
function todoApp(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      }
    default:
      return state
  }
}

Pure functions

  • Given the same input, will always return
    the same output
  • Produces no side effects
1
// impure addToCart mutates existing cart
const addToCart = (cart, item, quantity) => {
  cart.items.push({
    item,
    quantity
  });
  return cart;
};

// Pure addToCart() returns a new cart
// It does not mutate the original.
const addToCart = (cart, item, quantity) => {
  const newCart = lodash.cloneDeep(cart);

  newCart.items.push({
    item,
    quantity
  });
  return newCart;

};

NgRx

NgRx packages

Core Concepts

  • State accessed with the Store, an observable of state and an observer of actions.
  • State changes are handled by pure functions called reducers that take the current state and the latest action to compute a new state. 
  • Actions describe unique events that are dispatched from components and services.
  • Selectors are pure functions used to select, derive and compose pieces of state
  • Effects are an RxJS powered side effect model for Store. They use to update state based on external interactions such as network requests, web socket messages and time-based events.

Getting started

@NgModule({
  imports: [
    // ...
    StoreModule.forRoot({ featureKey: featureReducer }, { metaReducers }),
    StoreRouterConnectingModule.forRoot(),
    EffectsModule.forRoot([ AppEffects, FeatureEffects, /* ... */ ]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
    // ...
  ],
  declarations: []
})
export class AppModule {}
@NgModule({
  imports: [
    // ...
    StoreModule.forFeature({ otherFeatureKey: otherFeatureReducer }),
    EffectsModule.forFeature([ FeatureEffects ]),
    // ...
  ],
  declarations: []
})
export class LazyLoadedModule {}

1. Root configuration

2. Feature configuration

Actions

Writing actions

import { createAction } from '@ngrx/store';

export const homeScore = createAction('[Scoreboard Page] Home Score');
export const awayScore = createAction('[Scoreboard Page] Away Score');
export const setScores = createAction(
  '[Scoreboard Page] Set Scores',
  props<{ home: number; away: number }>()
);

Dispatching actions

import { Store } from '@ngrx/store';

@Component({ /* ... */ })
export class ScoreboardComponent {

  constructor(private store: Store<ScoreState>) {}

  setScores(home, away) {
    this.store.dispatch(setScores({ home, away }));
  }

}

Reducers

Implementing reducer

import { Action, createReducer, on } from '@ngrx/store';
import * as ScoreboardPageActions from '../actions/scoreboard.actions';

export interface ScoreState {
  home: number;
  away: number;
}

export const initialState: ScoreState = {
  home: 0,
  away: 0,
};

const scoreboardReducer = createReducer(
  initialState,
  on(ScoreboardPageActions.homeScore, state => ({ ...state, home: state.home + 1 })),
  on(ScoreboardPageActions.awayScore, state => ({ ...state, away: state.away + 1 })),
  on(ScoreboardPageActions.resetScore, state => ({ home: 0, away: 0 })),
  on(ScoreboardPageActions.setScores, (state, { game }) => ({ home: game.home, away: game.away }))
);

export function reducer(state: ScoreState | undefined, action: ScoreboardAction) {
  return scoreboardReducer(state, action);
}

Selectors

Using selector

import { createSelector } from '@ngrx/store';
 
export interface User {
  id: number;
  name: string;
}
 
export interface Book {
  id: number;
  userId: number;
  name: string;
}
 
export interface AppState {
  selectedUser: User;
  allBooks: Book[];
}
 
export const selectUser = (state: AppState) => state.selectedUser;
export const selectAllBooks = (state: AppState) => state.allBooks;

export const getCount = createSelector(
  selectAllBooks,
  (allBooks: Book[]) => allBooks.length
);
 
export const selectVisibleBooks = createSelector(
  selectUser,
  selectAllBooks,
  (selectedUser: User, allBooks: Book[]) => {
    if (selectedUser && allBooks) {
      return allBooks.filter((book: Book) => book.userId === selectedUser.id);
    } else {
      return allBooks;
    }
  }
);
ngOnInit() {
  this.counter = this.store.select(getCount)
}

Effects

Using effects

@Injectable()
export class MovieEffects {
 
  loadMovies$ = createEffect(() => this.actions$.pipe(
    ofType('[Movies Page] Load Movies'),
    switchMap(() => this.moviesService.getAll()
      .pipe(
        map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
        catchError((error) => ({ type: '[Movies API] Movies Loaded Failed', payload: error }))
      ))
    )
  );
 
  constructor(
    private actions$: Actions,
    private moviesService: MoviesService
  ) {}
}
@Injectable()
export class MovieEffects {
 
  @Effect()
  loadMovies$ = this.actions$
    .pipe(
      ofType('[Movies Page] Load Movies'),
      switchMap(() => this.moviesService.getAll()
        .pipe(
          map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
          catchError((error) => ({ type: '[Movies API] Movies Loaded Failed', payload: error }))
        ))
      )
    );
 
  constructor(
    private actions$: Actions,
    private moviesService: MoviesService
  ) {}
}

Demo

Useful links

Q & A

Made with Slides.com