Skip to content
No description, website, or topics provided.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
e2e
src
.editorconfig
.gitignore
README.adoc
angular.json
package-lock.json
package.json
tsconfig.json
tslint.json

README.adoc

NgRx Workshop

These are notes and code snippets from the NgRx-part of the Udemy Workshop The complete guide to Angular, which is very much recommended. My notes to the rest of the topics from the guide can be found here.

NgRx

  • reactive extensions for Angular

  • alternative to storing state in an Angular application

  • traditional way of using Subject in services = good thing because making sure that statechange only in this service + notification to rest of application

  • however: maintaining state in complex applications can become burdensome

  • different approach in NgRx: store state globally in key-value-pairs in one place + components can subscribe for changes

Redux-like pattern

  • like in React: only one central Store in application

  • changing state through Actions that gets send to Reducers

  • state-change immutable: old state doesn’t get overwritten but archived

Creating a Reducer

  • reducers add a new state

  • parameters will be passed in function automatically by NgRx

  • reducer-function will be triggered whenever an action is dispatched

  • arguments of reducer-function:

    1. State before the function is entered. If no state present, initial state can be determined. The spread-operator "…​" means "take the old state"

    2. action that overrides the specified part of the old state

  • so basically the following says "take the old state and override beverage with the payload from the action"

bar-reducers.ts:

import * as BarActions from './my.actions';
import {Beverage} from './beverage';

const initialState = {
  beverage: new Beverage('Club Mate', 1)
};

export function beverageReducer(state = initialState, action: BarActions.AddBeverage) {

  switch (action.type) {
    case BarActions.ADD_BEVERAGE:
      return {
        ...state,
        beverage: action.payload
      };
    default:
      return state;
  }
}

bar-actions.ts:

import {Action} from '@ngrx/store';
import {Beverage} from './beverage';

export const ADD_BEVERAGE = 'ADD_BEVERAGE';

export class AddBeverage implements Action {

  constructor(public payload: Beverage) {
  }

  readonly type = ADD_BEVERAGE;
}

export type BarActions = AddBeverage;
  • The constant ADD_BEVERAGE is internally used by NgRx to identify actions. Actions with the same identifier will be triggered all after another!

app.module.ts:

imports: [
  ...
  StoreModule.forRoot({beverageReducers: beverageReducer})
]

(Attention: There’s a best practice to centrally manage all reducers in an own file, see below)

Getting state & Dispatch Actions

  • in constructor: global state has to be defined as JSON-object

  • getting state: this.store.select

  • dispatching actions: this.store.dispatch

export class AppComponent {

  valueFromStore: Beverage;

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

  onInputNewValue(form: NgForm) {

    const beverage = new Beverage(form.value.name, form.value.price);

    console.log('Writing to store:');
    console.log(beverage);

    this.store.dispatch(new BarActions.AddBeverage(beverage));

    console.log('Reading from store ...');

    this.store.select('beverageReducers').subscribe(stateObject => {
      this.valueFromStore = stateObject.beverage;
    });

  }

Enhance Readability by using Interfaces for the State

my.reducers.ts:

export interface AppState {
  beverageModule: State;
}

export interface State {
  beverages: Beverage[];
  totalPrice: number;
}

const initialState: State = {
  beverages: [new Beverage('Club Mate', 0)],
  totalPrice: 0
};

used in app.component.ts:

constructor(private store: Store<fromBeverageModule.AppState>) {}

(instead of having to copy the specific state like this previous version (pretty simple state, but imagine it gets more complicated)):

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

Dealing with Observables by using async-Pipe

  • the whole state of a store is always returned as an observable, hence simple string values cannot be used in template like this:

This is my value: {{value}}
  • instead, async-pipe has to be used to get rid of the observable:

This is my value: {{value | async}}

Here’s how fields of a state can be accessed:

  • state definition in reducer-file:

export interface State {
  beverages: Beverage[];
  totalPrice: number;
}
  • usage: State itself is an observable, fields can be accessed via subscribe():

    this.store.select('bar').subscribe(stateObject => {
      this.valueFromStore = stateObject.beverages;
      this.totalPrice = stateObject.totalPrice;
    });

Always work on copy of state

  • when working with NgRx, past states must not be changed

  • instead, create copy of it and work on thi s copy

  • example: in reducer when removing a beverage, don’t change a past state this way:

const beverages = state.beverages; // DON`T DO THAT!
beverages.splice(action.payload, 1);
  • instead, create a copy of the past state to work on:

const beverages = [...state.beverages]; // get old beverages in an immutable way
beverages.splice(action.payload, 1);
  • uses the spread-operator which spreads the content of an array

  • additionally, the spread contents are framed by an array which makes it the same datatype as past state

  • also works for objects:

const oldStuff = {...state.stuff[index]};

Asynchronous Operations

  • for example: storing authentication token in store generally good idea; but getting this token from server will be asynchronous

  • problem: asynchronous operations cannot be handled by reducers!

Centrally manage State and Reducers

  • good practice: manage AppState-interface and reducers in a file like /store/app.reducers.ts:

import * as fromBarAndPubSupplier from '../bar-and-pub-supplier/store/bar-and-pub-supplier-reducers';
import * as fromBar from '../bar/store/bar-reducers';
import {ActionReducerMap} from '@ngrx/store';

export interface AppState {
  bar: fromBar.State;
  barAndPubSupplier: fromBarAndPubSupplier.State;
}

export const reducers: ActionReducerMap<AppState> = {
  bar: fromBar.barReducer,
  barAndPubSupplier: fromBarAndPubSupplier.barAndPubSupplierReducer
};
  • referenced in app.module.ts:

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    StoreModule.forRoot(reducers)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

take(1)

  • store.select() returns an Observable that can be subscribed to via subscribe():

this.store.select('barAndPubSupplier').subscribe(stateObject => {
  this.valueFromStore = stateObject.beverages;
  this.totalPrice = stateObject.totalPrice;
});
  • whenever store changes, subscription is fired

  • to have it fire only once to avoid unwanted side-effects:

this.store.pipe(select('barAndPubSupplier'), take(1)).subscribe(stateObject => {
  .take(1)
  .subscribe(stateObject => {
    this.valueFromStore = stateObject.beverages;
    this.totalPrice = stateObject.totalPrice;
});

Effects

  • when using REST API to get data that has to be put in store: call = asynchronous side-effect

  • easy solution: call REST API in service class, subscribe to result, then put result to store

  • more elegant alternative in NgRX: effects

npm install --save @ngrx/effects
  • (no example code in the code base of this repository because no backend present)

  • effects in file backendcall.effects.ts:

@Injectable()
export class BackendcallEffects {

  @Effect()
  importantEntities = this.actions$.pipe(
    ofType(BackendcallActions.MY_BACKENDCALL)
    .map((action: BackendcallActions.MY_BACKENDCALL) => {
      return action.payload;
    })

    // Do other stuff with the payload here
    // At the very end, an action has to be emitted to change the state of the store!

    ;
  );

  constructor(private actions$: Actions) {
  }

}
  • note that actions$ is postfixed with a $ because it’s an observable

  • also add EffectsModule to app.module.ts in imports-array:

...
imports: [
  ...
  EffectsModule.forRoot([BackendcallEffects])
],
...
  • usage of effects in some component.ts:

this.store.dispatch(new BackendcallActions.doBackendCall({value: 'value'}));
  • the action dispatched this way is kind of listened to in BackendcallEffects

  • effects are like reducers that don’t necessarily change the state of the store

  • with effects, asynchronous actions can just be dispatched like synchronous ones

Local UI State

  • Good best practice: Not every state has to be managed with NgRx. If a isolated component needs a state that could be provided by the component itself, it’s OK to do so instead of creating a new state in the store.

  • NgRx' strength is to provide state for multiple components

@ngrx/router-store

  • current routing is also a state of the application!

  • add to imports in app.module.ts:

imports: [
  ...
  StoreRouterConnectingModule
]

Lazy Loading

  • problem with lazy-loaded modules: AppState for whole application (consisting of the state of multiple modules) cannot be build at application startup because modules are not loaded

  • solution: inject new state dynamically when modules are actually loaded

  • in app.module.ts:

...
imports: [
  ...
  StoreModule.forFeature('myFeature', myReducer)
],
...
  • this will dynamically inject the state from myFeature into the global state when the module is loaded

Redux Devtools

  • Browser extension for Chrome to debug in browser

  • install with

    npm install --save @ngrx/store-devtools
  • …​ which results in changes in package.json:

  "dependencies": {
    "@ngrx/store-devtools": "^7.2.0",
    ...
  • app.modules.ts:

  imports: [
    ...
    StoreDevtoolsModule.instrument({
      maxAge: 200, // Retains last 25 states
      logOnly: environment.production, // Restrict extension to log-only mode
    }),
    StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }),
  ],
  ...
  • important: has to be added AFTER StoreModule

Further Reading

Always use NgRX?

  • Angular Service Layers: Redux, RxJs and Ngrx Store - When to Use a Store and why:

    • stores not viewed by their creators as a one-size-fits-all-solution: "You’ll know when you need Flux. If you aren’t sure if you need it, you don’t need it."

  • When should I use Redux?:

    • "I would like to amend this: don’t use Redux until you have problems with vanilla React."

    • "However, it’s also important to understand that using Redux comes with tradeoffs. It’s not designed to be the shortest or fastest way to write code. It’s intended to help answer the question "When did a certain slice of state change, and where did the data come from?", with predictable behavior."

What data to put into the store?

  • Which types of state should be placed in my NGRX store?:

    • global reference data that has to be cached in the browser, for example shopping cart or wizard progress

    • complex component interactions that would be hard to implement using bindings and event emitters

    • "Any properties of services in Angular (or AngularJs) are strong candidates to be placed into the store."

Is global State with NgRx an antipattern?

  • Managing global state with NgRx store in Angular

  • "Global state has had a bad reputation since inception due to its unpredictable nature. About two years ago, Redux was introduced as a way to manage this unpredictability by making the state immutable and operations acting on the state synchronous and stateless (a similiar approach can be found in the actor pattern). Since then, its principales has inspired multiple implementations, one of them being the Ngrx store for Angular."

  • Redux and global state vs. local state

You can’t perform that action at this time.