NgRx

Tips & Tricks

VOXXED DAYS FRONTEND

22 May 2019

Adrian Fâciu

Redux implementation for Angular

NgRx

@adrianfaciu

Redux? This again...

@adrianfaciu

Version 8!

@adrianfaciu

what is redux?

@adrianfaciu

what is redux?

Redux is a predictable state container for JavaScript apps

@adrianfaciu

@adrianfaciu

what is redux?

NgRx?

@adrianfaciu

what is NgRx?

+

+

@adrianfaciu

Side effects

@adrianfaciu

@adrianfaciu

Side effects

and now, some tips...

@adrianfaciu

Boilerplate

@adrianfaciu

createAction

@adrianfaciu

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


const logout = createAction(
  '[Toolbar] Logout'
);

const login = createAction(
  '[Login Page] Login',
  props<{ username: string; password: string }>()
);

createAction

@adrianfaciu

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


const logout = createAction(
  '[Toolbar] Logout'
);

const login = createAction(
  '[Login Page] Login',
  props<{ username: string; password: string }>()
);

createAction

@adrianfaciu

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


const logout = createAction(
  '[Toolbar] Logout'
);

const login = createAction(
  '[Login Page] Login',
  props<{ username: string; password: string }>()
);

createReducer

@adrianfaciu

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

export const reducer = createReducer(
  initialState,
  on(LoginActions.logout, 
    () => ({ ...state, username: ''})),),
  on(LoginActions.login,
    (state, { username }) => ({ ...state, username })),
);

createReducer

@adrianfaciu

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

export const reducer = createReducer(
  initialState,
  on(LoginActions.logout,
    () => ({ ...state, username: ''})),
  on(LoginActions.login,
    (state, { username }) => ({ ...state, username })),
);

createEffect

@adrianfaciu

import { createEffect } from '@ngrx/effects';

export class UserEffects {
  userLogin$ = createEffect(() => this.actions$.pipe(
    ofType(UserActions.login),
    map(({user}) => AuthApiActions.login({user}))
  ))
}

createEffect

@adrianfaciu

import { createEffect } from '@ngrx/effects';

export class UserEffects {
  userLogin$ = createEffect(() => this.actions$.pipe(
    ofType(UserActions.login),
    map(({user}) => AuthApiActions.login({user})),
  ));
}

Built-in runtime checks

@adrianfaciu

@adrianfaciu

@NgModule({
  imports: [
    StoreModule.forRoot(reducerMap, {
      runtimeChecks: {
        strictImmutability: true,
        strictStateSerializability: true,
        strictActionSerializability: true,
      },
    }),
  ],
})
export class AppModule {}

Built-in runtime checks

@adrianfaciu

@NgModule({
  imports: [
    StoreModule.forRoot(reducers, {
      runtimeChecks: {
        strictImmutability: true,
        strictStateSerializability: true,
        strictActionSerializability: true,
      },
    }),
  ],
})
export class AppModule {}

Built-in runtime checks

@adrianfaciu

@NgModule({
  imports: [
    StoreModule.forRoot(reducers, {
      runtimeChecks: {
        strictImmutability: true,
        strictStateSerializability: true,
        strictActionSerializability: true,
      },
    }),
  ],
})
export class AppModule {}

Built-in runtime checks

Use descriptive strings for action types

@adrianfaciu

const logout = createAction(
  '[Toolbar] Logout'
);

const login = createAction(
  '[Login Screen] Login'
);


const fetchUsers = createAction(
  '[Admin Users List] Fetch users'
);

@adrianfaciu

Descriptive types

Keep all related actions in the same file

@adrianfaciu

// products.actions.ts

const fetch = 
  createAction('[Product List] Fetch');

const fetchSuccess =
  createAction('[Product List] Fetch Success');

const fetchFail =
  createAction('[Product List] Fetch Fail');

const fetchCount =
  createAction('[Product List] Fetch Count');

@adrianfaciu

Grouped actions

Not all effects have to emit something

@adrianfaciu

showSaveMessage$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(saveSuccess),
        tap(() => this.snackBar.open(
          'Saved!',
           null,
          { duration: 1000 }))
      ),
    {
      dispatch: false,
    }
  );

@adrianfaciu

No dispatch

showSaveMessage$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(saveSuccess),
        tap(() => this.snackBar.open(
          'Saved!',
           null,
          { duration: 1000 }))
      ),
    {
      dispatch: false,
    }
  );

@adrianfaciu

No dispatch

showSaveMessage$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(saveSuccess),
        tap(() => this.snackBar.open(
          'Saved!',
           null,
          { duration: 1000 }))
      ),
    {
      dispatch: false,
    }
  );

@adrianfaciu

No dispatch

Not all effects have to be based on actions stream

@adrianfaciu

showSaveMessage$ = createEffect(
    () =>
      fromEvent(document, 'click').pipe(
        map(() => UserActions.click())
      )
);

@adrianfaciu

No actions stream

showSaveMessage$ = createEffect(
    () =>
      fromEvent(document, 'click').pipe(
        map(() => UserActions.click())
      )
);

@adrianfaciu

No actions stream

Always handle errors in effects

@adrianfaciu

  loadEmployees$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fetch),
      switchMap(() =>
        this.employeeService.getAll().pipe(
          map(employees => fetchSuccess({ employees })),
          catchError(() => of(fetchError()))
        )
      )
    )
  );

@adrianfaciu

Catch errors

@adrianfaciu

Catch errors

  loadEmployees$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fetch),
      switchMap(() =>
        this.employeeService.getAll().pipe(
          map(employees => fetchSuccess({ employees })),
          catchError(() => of(fetchError()))
        )
      )
    )
  );

Beware of switchMap

@adrianfaciu

Beware of switchMap

@adrianfaciu

  deleteEmployees$ = createEffect(() =>
    this.actions$.pipe(
      ofType(delete),
      switchMap(({id}) =>
        this.employeeService.delete(id)
          .pipe(
             map(employees => deleteSuccess()),
             catchError(() => of(deleteError()))
        )
      )
    )
  );

Use selectors

@adrianfaciu

Selectors

Pure functions, used to get slices of state

@adrianfaciu

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

export const selectFeature =
  (state: AppState) => state.feature;

export const selectCount = createSelector(
  selectFeature,
  (state: FeatureState) => state.counter
);

@adrianfaciu

Use selectors

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

export const selectFeature =
  (state: AppState) => state.feature;

export const selectCount = createSelector(
  selectFeature,
  (state: FeatureState) => state.counter
);

@adrianfaciu

Use selectors

@adrianfaciu

Use selectors

  • Type-safe

@adrianfaciu

Use selectors

  • Type-safe
  • Composition

@adrianfaciu

Use selectors

  • Type-safe
  • Composition
  • Memoization

Selectors with props

@adrianfaciu

Selectors with props

@adrianfaciu

export const getCount = createSelector(
  getCounterValue,
  (counter, props) => counter * props.multiply
);


this.counter =
  this.store.select(getCount, { multiply: 2 });

Selectors with props

@adrianfaciu

export const getCount = createSelector(
  getCounterValue,
  (counter, props) => counter * props.multiply
);


this.counter =
  this.store.select(getCount, { multiply: 2 });

Simple reducers

@adrianfaciu

  • no hidden logic, they should only update the state

@adrianfaciu

Simple reducers

  • no hidden logic, they should only update the state
  • update only one small part of the state

@adrianfaciu

Simple reducers

  • no hidden logic, they should only update the state
  • update only one small part of the state
  • don't mutate state, always create new objects

@adrianfaciu

Simple reducers

@adrianfaciu

Simple reducers

export const reducer = createReducer(
  initialState,
  on(ScoreboardPageActions.homeScore,
    (state) => {...state, home: state.home + 1}),
  on(ScoreboardPageActions.awayScore,
    (state) => {...state, away: state.away + 1}),
  on(ScoreboardPageActions.reset,
    (state, {away, home}) => {...state, away, home}),
);

Normalize data

@adrianfaciu

  • avoid deeply nested objects

@adrianfaciu

Normalize data

  • avoid deeply nested objects

  • don't duplicate

@adrianfaciu

Normalize data

  • avoid deeply nested objects

  • don't duplicate

  • store as objects, not as arrays

@adrianfaciu

Normalize data

{
  articles: {
    123: {
      id: 123,
      author: 1,
      title: "My awesome blog post",
      comments: [ 324 ]
    }
  },
  users: {
    1: { id: 1, name: "Paul" },
    2: { id: 2, name: "Nicole" }
  },
  comments: {
    324: { id: 324, author: 2 }
  } 
}

@adrianfaciu

{
  articles: {
    123: {
      id: 123,
      author: 1,
      title: "My awesome blog post",
      comments: [ 324 ]
    }
  },
  users: {
    1: { id: 1, name: "Paul" },
    2: { id: 2, name: "Nicole" }
  },
  comments: {
    324: { id: 324, author: 2 }
  } 
}

@adrianfaciu

{
  articles: {
    123: {
      id: 123,
      author: 1,
      title: "My awesome blog post",
      comments: [ 324 ]
    }
  },
  users: {
    1: { id: 1, name: "Paul" },
    2: { id: 2, name: "Nicole" }
  },
  comments: {
    324: { id: 324, author: 2 }
  } 
}

@adrianfaciu

Don’t use the store all over the place

@adrianfaciu

  • This is mainly about containers and dumb components

@adrianfaciu

Containers

  • This is mainly about containers and dumb components
  • Not everyone should know about the store

@adrianfaciu

Containers

  • This is mainly about containers and dumb components
  • Not everyone should know about the store
  • Group several dumb components inside a container that handles the logic

@adrianfaciu

Containers

NgRx

AiD

@adrianfaciu

@adrianfaciu

VoxxedDays: NgRx

By Adrian Faciu

VoxxedDays: NgRx

  • 2,267