Skip to content
This repository has been archived by the owner on Dec 30, 2019. It is now read-only.

Commit

Permalink
@ngrx/* update, including required code adoptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
DorianGrey committed Aug 18, 2017
1 parent 96a2d14 commit 200cb00
Show file tree
Hide file tree
Showing 16 changed files with 416 additions and 205 deletions.
81 changes: 45 additions & 36 deletions docs/app_state.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# The application state
The application's globally relevant state is stored using a store from the [ngrx/store](https://github.com/ngrx/store) library. It's content is described in `src/app.store.ts`, in the interface `AppState`:
The application's core state is stored using a store from the [ngrx/store](https://github.com/ngrx/store) library. It's content is described in `src/app.store.ts`, in the interface `AppState`:

export interface AppState {
todos: List<Todo>;
watchTime: number;
export interface CoreAppState {
todos: TodoState;
language: LanguageState;
router: RouterReducerState;
}

It is not required to define an interface for describing your application state, but it simplifies type safe injection of the application's store. E.g., in `src/todos/todo.service.ts`, you will see an injection like this:
Expand All @@ -14,14 +15,15 @@ Without defining an explicit interface, it would be required to use `any` here.

To properly work with your application state during runtime, you need to define a set of `reducers` for each of its entries. In our case, this is also defined in `src/app.store.ts`:

const reducers = {
todos: todosReducer,
watchTime: watchTimeReducer
export const reducers: ActionReducerMap<CoreAppState> = {
todos: todosReducer,
language: languageReducer,
router: routerReducer
};

This hash of reducers gets combined to a single root reducer using the `combineReducers` helper function from `ngrx/store`.
This hash of reducers gets combined to a single root reducer using `StoreModule.forRoot` (see `src/app/app.imports.ts`).

You might have recognized that the name of the keys used in the reducers object and the interface definition is equivalent. This is *intended*, since it simplifies to understand how an entry of the interface is mapped to its counterpart in the reducers objects. We strongly recommend to keep follow this convention.
You might have recognized the `ActionReducerMap<CoreAppState>` interface. Technically, it's a helper to ensure that the hash contains a reducer for exactly every field of `CoreAppState` - if you don't, the transpiler will comply and crash you build.

Please read the source documentation in `src/app.store.ts` to get more detailed information about the values and structures used in there.

Expand All @@ -30,17 +32,32 @@ Extending the application's state is easier than it appears - we'll go through t

First, you should create a `[component-or-module-name].store.ts` along the component or module file that this part of the application state belongs to or is primarily used by. State parts that do not refer to a particular component or module should be added to the global definitions in `src/app.store.ts`.

To properly deal with the state itself, you need to define a list of actions that may alter that state. It most cases, it is sufficient to use an enum or a hash with string fields inside. E.g., for the `todos` page, we've use the latter version like:
To properly deal with the state itself, you need to define a list of actions that may alter that state. First of all, you need to define some named actions that illustrate the potential modifications. An example from the `todos` part of the store:

export const ACTION_TYPES = {
const ACTION_TYPES = {
ADD_TODO: "ADD_TODO"
};

In case of the hash strategy, don't forget to properly freeze this object to prevent its accidental modification:

Object.freeze(ACTION_TYPES);

This is not required for (const) enums, since they cannot be modified during runtime. However, this might need some more `.toString()` calls, since the actions dispatched by the stored have to be identified by strings.
To get the most out of types, it is recommended to use classes to represent you actions. The `type` should be turned into a `readonly` field to avoid accidental modification.

export class AddTodoAction implements Action {
readonly type = ACTION_TYPES.ADD_TODO;
constructor(public payload: Todo) {}
}
Please note that since ngrx v4, the `Action` interface no longer contains a `payload`, so you have to define it yourself as illustrated below. However, it is recommended to stick to this naming convention.

Next, define a type alias with a union of all potential action types to properly type you reducer. Example from `src/app/todos/todos.store.ts`:

export type TodoActions = AddTodoAction | CompleteTodoAction;

Furthermore, you should define an interface or a type alias describing your state part's type.

export interface State {
current: List<Todo>;
completed: List<Todo>;
}

This simplifies defining your reducer (later) and composing your part with the others.

Next, you need to define an initial value for your state. This value will be used when your application gets started, before the first dispatch is executed. In the example above, we've used an empty list of `Todo` entries:

Expand All @@ -50,43 +67,35 @@ For those who argued: This template uses [immutable-js](https://facebook.github.

Go ahead with defining a proper `reducer` for your state. A `reducer` receives two parameters:
- The current state for the particular entry.
- The requested action. This parameters contains two fields:
- `type` is one of your defined actions names. In the example above, `"ADD_TODO"` would be the only possible value. Take care that you define your initial state as the default value of this parameter to get things to properly work on startup and hot reload.
- `payload` is an optional value referring to this action. In the example above, this would be a `Todo` instance that should be added to the list of todos.
- The requested action. If you have followed the convention above when defining your actions, it will have some fields:
- `type` is one of your defined action's name. In the example above, `"ADD_TODO"` would be the only possible value. Take care that you define your initial state as the default value of this parameter to get things to properly work on startup and hot reload.
- `payload` is an optional value referring to this action. In the example above, this would be a `Todo` instance that should be added to the list of todos. Please keep in mind that you might have to type-cast this field, since you're asserting a union type.

The reducer is responsible for properly evaluating these parameters and - in case it accepts the provided action type - returning a new application state. If you want things to work in a reasonable manner, you should take care of two aspects:
1. **Never** alter the state that is provided here!
2. **Always** return a new object containing your state!

If you want to omit at least one of these aspects... don't tell anyone you've not been warned.

In the example mentioned above, the reducer looks like:
In the example mentioned above, the reducer looks like (make sure to export it as a function to remain AoT conforming):

export const todosReducer: ActionReducer<any> = (state: List<Todo> = initialTodoList, action: Action) => {
export function todosReducer(
state: State = initialTodoList, action: TodoActions
): State {
switch (action.type) {
case ACTION_TYPES.ADD_TODO:
return state.push(action.payload);
default:
return state;
}
};

}

As the last step, add your state part to the global `AppState` definition and the corresponding reducer to the global one. Oh, and take care that your reducer and your set of action types is properly exported, to be usable outside of the definition file.

Once you did all this stuff, you are ready to select your new state part from the injected `Store` instance:

store.select(state => state.todos)

# Optional: Use action creators

While exploring the file that we picked the examples from, you might have recognized a so-called `ActionCreator`:

export class TodoActionCreator {
add: (todo: Todo) => Action = todo => {
return {type: ACTION_TYPES.ADD_TODO, payload: todo};
};
}

First of all, using these constructs is entirely optional. However, it simplifies the process of creating mutation actions for your stated, since it hides the concrete structure of the action that gets dispatched by the store. Also, it adds some expressiveness.
# State extension using feature modules

If you decide to use these, don't forget to add them as providers, so that you can properly inject them. Alternatively, since the action creators themselves do not contain any kind of state, you can boil them down to simple helper functions placed in your module.
Since ngrx v4, it is possible to extend the application store during runtime when loading feature modules. Please see
`src/app/+lazy-test/lazy-test.store.ts` and `src/app/+lazy-test/lazy-test.module.ts` for further details and explanations - the steps are almost equivalent to the ones for extending the core state, except that the definition must not be added to the root in `app.store.ts`.
8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@angular/platform-server": "^4.3.4",
"@angularclass/hmr": "^2.1.3",
"@angularclass/hmr-loader": "^3.0.4",
"@ngrx/store-devtools": "^3.2.4",
"@ngrx/store-devtools": "^4.0.0",
"@ngtools/webpack": "^1.6.0",
"@types/jasmine": "^2.5.53",
"@types/lodash-es": "^4.14.6",
Expand Down Expand Up @@ -135,17 +135,15 @@
"@angular/platform-browser": "^4.3.4",
"@angular/platform-browser-dynamic": "^4.3.4",
"@angular/router": "^4.3.4",
"@ngrx/core": "1.2.0",
"@ngrx/router-store": "^1.2.6",
"@ngrx/store": "^2.2.3",
"@ngrx/router-store": "^4.0.4",
"@ngrx/store": "^4.0.3",
"@ngx-translate/core": "^7.2.0",
"@types/lodash": "^4.14.72",
"angular-router-loader": "^0.6.0",
"core-js": "^2.5.0",
"immutable": "3.8.1",
"lodash-es": "^4.17.4",
"normalize.css": "^7.0.0",
"reselect": "^3.0.1",
"rxjs": "5.4.2",
"zone.js": "^0.8.16"
},
Expand Down
12 changes: 9 additions & 3 deletions src/app/+lazy-test/lazy-test.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import { SharedModule } from "../shared/shared.module";

import { LAZY_TEST_ROUTES } from "./lazy-test.routes";
import { LazyTestComponent } from "./lazy-test.component";
import { LazyTestActionCreator } from "./lazy-test.store";
import { LAZY_TEST_FEATURE_NAME, watchTimeReducer } from "./lazy-test.store";

import { LazyTestService } from "./lazy-test.service";
import { StoreModule } from "@ngrx/store";

@NgModule({
imports: [LAZY_TEST_ROUTES, SharedModule],
imports: [
LAZY_TEST_ROUTES,
SharedModule,
StoreModule.forFeature(LAZY_TEST_FEATURE_NAME, watchTimeReducer)
],
declarations: [LazyTestComponent],
providers: [LazyTestService, LazyTestActionCreator]
providers: [LazyTestService]
})
export class LazyTestModule {}
14 changes: 7 additions & 7 deletions src/app/+lazy-test/lazy-test.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { Injectable } from "@angular/core";
import { Store } from "@ngrx/store";
import { Observable } from "rxjs/Observable";

import { AppState, getWatchTime } from "../app.store";
import { LazyTestActionCreator } from "./lazy-test.store";
import {
getWatchTime,
LazyTestStateSlice,
IncrementSecondsAction
} from "./lazy-test.store";

@Injectable()
export class LazyTestService {
watchTime: Observable<number>;

constructor(
private store: Store<AppState>,
private actionCreator: LazyTestActionCreator
) {
constructor(private store: Store<LazyTestStateSlice>) {
this.watchTime = this.store.select(getWatchTime);
}

updateSeconds() {
this.store.dispatch(this.actionCreator.increaseWatchTimeSecond());
this.store.dispatch(new IncrementSecondsAction());
}
}
92 changes: 81 additions & 11 deletions src/app/+lazy-test/lazy-test.store.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,97 @@
import { Action, ActionReducer } from "@ngrx/store";
// tslint:disable max-classes-per-file
import { Action } from "@ngrx/store";

export const LAZY_TEST_ACTION_TYPES = {
/**
* Define a set of constants that represent the actions that
* your locally defined reducer can deal with.
*
* Note: As long as you are using strongly-typed actions using
* classes as illustrated in the definitions below, there is
* no need to export this definition. If you ever want to do so,
* it is recommended to freeze it using `Object.freeze`.
*/
const LAZY_TEST_ACTION_TYPES = {
INC_SECONDS: "INC_SECONDS"
};

Object.freeze(LAZY_TEST_ACTION_TYPES);
/**
* Since @ngrx/store v4, the `Action` interface no longer contains a `payload`
* field. To get the best out of action definitions, it is recommended
* to use classes for them - this breaking change makes code tend towards
* this practice.
* It is considered best practice to override the `type` field with a readonly
* one, which carries an entry of the definition set on top of this file.
* If you want add a field containing your payload, just use "payload" for
* conventionally naming it.
*
* Using these kind of actions is pretty straight forward:
*
* store.dispatch(new YourAction(yourOptionalPayload))
*/
export class IncrementSecondsAction implements Action {
readonly type = LAZY_TEST_ACTION_TYPES.INC_SECONDS;
}

/**
* Put all of your actions together in a union type to optimize you reducer's accepted
* action type. It's trivial in this case, but might get more complicated later on.
*
* Feel free to add more actions if required.
*/
export type LazyTestActions = IncrementSecondsAction;

/**
* Export a type alias or interface describing the type of the store-part you're defining
* here. It's recommended in general to do so for more complex entries, and I recommend
* it as well for more simple structures to ensure consistency.
*/
export type State = number;

/**
* This file defines the utility required for a store entry added by a feature module.
* The name of this feature has to be provided as a string constant, which will turn
* into the name of the store entry.
* Names like this should be defined in the definition of the store part, so that anyone
* accessing it from external can just pick up the constant and has no need to duplicate
* this value.
*/
export const LAZY_TEST_FEATURE_NAME = "watchTime";

export class LazyTestActionCreator {
increaseWatchTimeSecond: () => Action = () => {
return { type: LAZY_TEST_ACTION_TYPES.INC_SECONDS };
};
/**
* The slice of the application state defined by this module.
* One this module got loaded, the fields defined by it will
* be added to the application's store, i.e. the resulting
* state is of type {CoreAppState & LazyTestStateSlice}.
*
* Keep in mind that the name defined here has to match the content of the constant
* above - otherwise, this value won't be found during runtime.
*/
export interface LazyTestStateSlice {
watchTime: State;
}

/**
* A pre-defined selector for retrieving the `watchTime` from the application's state.
*/
export const getWatchTime = (state: LazyTestStateSlice) => state.watchTime;

/**
* The initial state for this reducer. Used in the reducer definition below.
* Note that it is also possible to define the initial state globally via `StoreModule.forRoot`.
* It's a matter of personal preference if you prefer that centralized approach or the more
* localized one used in this template.
*/
const initialWatchTime = 0;

export const watchTimeReducer: ActionReducer<number> = (
export function watchTimeReducer(
state: number = initialWatchTime,
action: Action
) => {
action: LazyTestActions
): State {
switch (action.type) {
case LAZY_TEST_ACTION_TYPES.INC_SECONDS:
return state + 1;
default:
return state;
}
};
}
// tslint:enable max-classes-per-file
11 changes: 5 additions & 6 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import indexOf from "lodash-es/indexOf";
import { Component } from "@angular/core";
import { Store } from "@ngrx/store";
import { TranslateService } from "@ngx-translate/core";

import { AppState, getLanguage } from "./app.store";
import { Observable } from "rxjs/Observable";
import { LangActionCreator } from "./i18n/language.store";

import { CoreAppState, getLanguage } from "./app.store";
import { SetLanguageAction } from "./i18n/language.store";

@Component({
selector: "app-root",
Expand All @@ -19,8 +19,7 @@ export class AppComponent {

constructor(
private translate: TranslateService,
private store: Store<AppState>,
private langCreator: LangActionCreator
private store: Store<CoreAppState>
) {
this.currentLanguage = this.store.select(getLanguage);
this.availableLanguages = this.translate.getLangs();
Expand All @@ -33,7 +32,7 @@ export class AppComponent {
this.availableLanguages.length;
const nextLang = this.availableLanguages[idx];
this.translate.use(nextLang);
this.store.dispatch(this.langCreator.setLang(nextLang));
this.store.dispatch(new SetLanguageAction(nextLang));
});
}
}
10 changes: 5 additions & 5 deletions src/app/app.imports.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BrowserModule } from "@angular/platform-browser";

import { StoreModule } from "@ngrx/store";
import { RouterStoreModule } from "@ngrx/router-store";
import { StoreRouterConnectingModule } from "@ngrx/router-store";

import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { createTranslateLoader } from "./translate.factory";
Expand All @@ -10,7 +10,7 @@ import { APP_ROUTES } from "./app.routes";
import { SharedModule } from "./shared/shared.module";
import { InputTestModule } from "./input-test/input-test.module";
import { TodosModule } from "./todos/todos.module";
import { rootReducer } from "./app.store";
import { reducers, metaReducers } from "./app.store";
import { StoreDevtoolsModule } from "@ngrx/store-devtools";

/*
Expand All @@ -30,16 +30,16 @@ const imports = [
SharedModule.forRoot(),
InputTestModule,
TodosModule,
StoreModule.provideStore(rootReducer),
RouterStoreModule.connectRouter()
StoreModule.forRoot(reducers, { metaReducers }),
StoreRouterConnectingModule
];

/*
Note: We only consider this extension to be useful in development mode.
If you want to use in production as well, just remove the ENV-specific condition.
*/
if ("production" !== process.env.NODE_ENV) {
imports.push(StoreDevtoolsModule.instrumentOnlyWithExtension());
imports.push(StoreDevtoolsModule.instrument({ maxAge: 50 }));
}

export const APP_IMPORTS = imports;
Loading

0 comments on commit 200cb00

Please sign in to comment.