NgRx

Tips & Tricks

Adrian Fâciu

developer

NgRx

Redux implementation for Angular

Redux ? This again ...

what is redux ?

what is redux ?

Redux is a predictable state container for JavaScript apps

NgRx ?

what is NgRx ?

+

+

Side effects

 

and now, some tips...

Use classes for Actions

export const increment = { type: 'INCREMENT' };
export const increment = { 
    type: 'INCREMENT',
    payload: 10,
};
export const increment = createAction('INCREMENT');
export const decrement = createAction('DECREMENT');

increment(); // { type: 'INCREMENT' }
decrement(); // { type: 'DECREMENT' }

incement(10); // { type: 'INCREMENT', payload: 10 }
decrement([1, 42]); // { type: 'DECREMENT', payload: [1, 42] }

type safety ?

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

export const INCREMENT = 'INCREMENT';

export class Increment implements Action {
  readonly type = INCREMENT;
  public payload: number;

  constructor(payload: number) {
    this.payload = payload;
  }
}
import { Action } from '@ngrx/store';

export const INCREMENT = 'INCREMENT';

export class Increment implements Action {
  readonly type = INCREMENT;

  constructor(public payload: number) { }
}

Keep all related actions in the same file

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

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

export class Increment implements Action {
  readonly type = INCREMENT;
  constructor(public payload: number) { }
}

export class Decrement implements Action {
  readonly type = DECREMENT;
  constructor(public payload: number) { }
}

export type Actions = Increment | Decrement;

import * as appActions from './actions/app.actions';

// Dispatch
store.dispatch(new appActions.Increment(5));

// Reducer
export function Reducer(
    state: AppState,
    action: appActions.Actions,
) {
  switch (action.type) {
    case appActions.INCREMENT:
      return { counter: state.counter + action.payload };
    default:
      return state;
  }
}

Always use payload property

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

export const LOG_ERROR = 'LOG_ERROR';

export class LogError implements Action {
  readonly type = LOG_ERROR;
 
  constructor(public payload: { message: string }) { }
}

Use descriptive strings for action types

export const LOG_ERROR   = '[Logger] Log Error';

export const LOG_WARNING = '[Logger] Log Warning';

export const FETCH_LOGS  = '[Logger] Fetch';

Implement toPayload

function toPayload<T>(action: { payload: T }) {
    return action.payload;
}
const toPayload = <T>(action: { payload: T }) => action.payload;
@Effect()
public errors$: Observable<Action> = this.actions$
   .pipe(
      ofType(LOG_ERROR),
      map(toPayload),
      tap(payload => console.log(payload)),
      ...
    );

Not all effects have to emit something

@Effect({ dispatch: false })
public userLogout$: Observable<Action> = this.actions$
   .pipe(
      ofType(userActions.Logout),
      tap(action => console.log('Logging out', action)),
    );

Use selectors

export interface AppState {
    user: User;
    logs: Logs[],
    ....
}

export const getUser = (state: AppState) => state.user;

const user = store.select(getUser);
import { createSelector } from '@ngrx/store';

export const getUserLanguage = createSelector(
    getUser,
    (user) => user.language,
);

export const getUserLogs = createSelector(
    getUser,
    getLogs, 
    (user, logs) => logs.filter(log => log.userId === user.id),
);

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
function reducer(state = initialState, action: AuthActionsUnion) {
  switch (action.type) {
    case AuthActionTypes.LoginSuccess: {
      return {
        ...state,
        loggedIn: true,
        user: action.payload.user,
      };
    }

    case AuthActionTypes.Logout: {
      return initialState;
    }

    default: {
      return state;
    }
  }
}
export function reducer(
  state = initialState,
  action: BookActionsUnion | CollectionActionsUnion
): State {
  switch (action.type) {
    case BookActionTypes.SearchComplete:
    case CollectionActionTypes.LoadSuccess: {
      return {
        ...state,
        books: action.payload,
      };
    }

    case BookActionTypes.Load: {
      return {
        ...state,
        isLoading: true,
      };
    }

    case BookActionTypes.Select: {
      return {
        ...state,
        selectedBookId: action.payload,
      };
    }

    default: {
      return state;
    }
  }
}

Normalize data

  • don't duplicate

  • avoid (deeply) nested objects

  • store as objects, not as arrays

  • use helpers libraries if needed

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}
{
  result: "123",
  entities: {
    "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", "commenter": "2" }
    }
  }
}

Don’t use the store all over the place

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

Use Rx methods that end with 'to'

const first$ = stream$.pipe(map(_ => MY_CONSTANT));
const second$ = stream$.pipe(switchMap(() => of(MY_CONSTANT)));
const first$ = stream$.pipe(mapTo(MY_CONSTANT));
const second$ = stream$.pipe(switchMapTo(of(MY_CONSTANT)));
  • mapTo
  • switchMapTo
  • concatMapTo
  • mergeMapTo

Use the developer tools

Generic error action

export class ErrorOccurred implements Action {
    readonly type = ERROR_OCCURRED;
    constructor(
        public payload: {
            action?: Action;
            error?: ErrorData;
        },
    ) {}
}
@Effect()
loadDocuments$ = this.actions$.pipe(
   ofType<documentActions.Fetch>(documentActions.FETCH)
   switchMapTo(
       this.documentsService
         .getDocuments().pipe(
            map(docs => new documentActions.FetchSuccess(docs)),
            catchError(error => of(
                  new ErrorOccurred({
                      action: { type: documentActions.FETCH },
                      error,
                  })
              )),
          )
    )
)

If you want the written form there is a medium post

Questions ?

NgRx: Tips & Tricks

By Adrian Faciu

NgRx: Tips & Tricks

Some best practices when using NgRx

  • 977