NgRx Guidelines
This chapter gives guidance about NgRx. NgRx is a framework for building reactive applications in Angular.
OneCx uses NgRx to manage the state of an applet. The communication with the store is done by smart components, usually by the page components.
Guidelines for NgRx
-
An action is only dispatched at one place.
-
Actions are never dispatched conditionally.
-
For every component that is getting data from the store there is an interface for the viewmodel that is called like the component class but with postfix "ViewModel".
-
For every component that is getting data from the store there is a selector for the viewmodel that is called like the viewmodel interface but with the prefix "select".
-
Every component that is getting data from the store selects only the viewmodel via the viewmodel selector. The observable that is storing the result is called
viewModel$. -
Every HTML file of a component that is getting data from the store has an outer ng-container tag that is using the ngrxLet directive e.g.
*ngrxLet="viewModel$ as vm". -
The store never stores data that can be computed from other data in the store. This is done in selectors.
-
Selectors are only accessing the data on top-level of the part of the store that is past to it.
-
Every feature module has its own feature store.
-
The store is the model of the applet and is modeled like that. Data that is relevant for all pages in the feature are stored on top-level. Data that is only relevant for one page is stored in a subsection of the store which is called like the page.
-
All HTTP calls are done in effects.
-
Routing or changing the URL parameters via the angular router is done in effects.
-
Selectors, reducers and effects are tested.
-
For reducers there is at least a test for the initial state and for one state that is not the initial state.
-
Effects are tested via marble testing.
-
Selector tests are testing the projector function of the selectors.
Project structure
-
See example for NgRx project The following examples shows a potential folder structure, that might be extended based on the need.
| Folders should only be created and used if they have content. [Example Angular NgRx structure] |
├── src │ ├── app │ │ ├── featuremodule1 // one of feature modules, might have many of them │ │ │ ├── components // optional - components shared between pages of this feature module │ │ │ │ ├── featurecomponent1 │ │ │ │ │ ├── featurecomponent1.component.html │ │ │ │ │ ├── featurecomponent1.component.scss │ │ │ │ │ ├── featurecomponent1.component.spec.ts │ │ │ │ │ ├── featurecomponent1.component.ts │ │ │ ├── directives // optional - directives used in specific feature module │ │ │ ├── pages │ │ │ │ ├── page1 // every page gets a subfolder, might have many of them │ │ │ │ │ ├── page1subcomponent1 // one of subcomponent for page, optional + might have many of them │ │ │ │ │ │ ├── page1subcomponent1.component.html │ │ │ │ │ │ ├── page1subcomponent1.component.scss │ │ │ │ │ │ ├── page1subcomponent1.component.spec.ts │ │ │ │ │ │ ├── page1subcomponent1.component.ts │ │ │ │ │ │ ├── page1subcomponent1.viewmodel.ts │ │ │ │ │ ├── dialogs │ │ │ │ │ │ ├── page1dialog1 // dialog that is only used by this page, optional + might have many of them │ │ │ │ │ │ │ ├── page1dialog1.component.html │ │ │ │ │ │ │ ├── page1dialog1.component.scss │ │ │ │ │ │ │ ├── page1dialog1.component.spec.ts │ │ │ │ │ │ │ ├── page1dialog1.component.ts │ │ │ │ │ │ │ ├── page1dialog1.viewmodel.ts │ │ │ │ │ ├── page1.component.html │ │ │ │ │ ├── page1.component.scss │ │ │ │ │ ├── page1.component.spec.ts │ │ │ │ │ ├── page1.component.ts │ │ │ │ │ ├── page1.reducer.ts │ │ │ │ │ ├── page1.reducer.spec.ts │ │ │ │ │ ├── page1.selectors.ts │ │ │ │ │ ├── page1.selectors.spec.ts │ │ │ │ │ ├── page1.state.ts │ │ │ │ │ ├── page1.effects.spec.ts │ │ │ │ │ ├── page1.effects.ts │ │ │ │ │ ├── page1.actions.ts │ │ │ │ │ ├── page1.viewmodel.ts │ │ │ ├── dialogs // dialogs which are used across pages, optional │ │ │ │ ├── dialog1 // every dialog gets a subfolder, might have many of them │ │ │ │ │ ├── dialog1subcomponent1 // one of subcomponent for dialog, optional + might have many of them │ │ │ │ │ │ ├── dialog1subcomponent1.component.html │ │ │ │ │ │ ├── dialog1subcomponent1.component.scss │ │ │ │ │ │ ├── dialog1subcomponent1.component.spec.ts │ │ │ │ │ │ ├── dialog1subcomponent1.component.ts │ │ │ │ │ │ ├── dialog1subcomponent1.viewmodel.ts │ │ │ │ │ ├── dialog1.component.html │ │ │ │ │ ├── dialog1.component.scss │ │ │ │ │ ├── dialog1.component.spec.ts │ │ │ │ │ ├── dialog1.component.ts │ │ │ │ │ ├── dialog1.reducer.ts │ │ │ │ │ ├── dialog1.reducer.spec.ts │ │ │ │ │ ├── dialog1.selectors.ts │ │ │ │ │ ├── dialog1.selectors.spec.ts │ │ │ │ │ ├── dialog1.state.ts │ │ │ │ │ ├── dialog1.effects.spec.ts │ │ │ │ │ ├── dialog1.effects.ts │ │ │ │ │ ├── dialog1.actions.ts │ │ │ │ │ ├── dialog1.viewmodel.ts │ │ │ ├── featuremodule1.module.ts │ │ │ ├── featuremodule1.reducers.ts │ │ │ ├── featuremodule1.reducers.spec.ts │ │ │ ├── featuremodule1.routes.ts │ │ │ ├── featuremodule1.selectors.ts │ │ │ ├── featuremodule1.selectors.spec.ts │ │ │ ├── featuremodule1.state.ts │ │ ├── shared // (technical) components and directives used in multiple feature modules │ │ │ ├── components │ │ │ │ ├── sharedcomponent1 │ │ │ │ │ ├── sharedcomponent1.component.html │ │ │ │ │ ├── sharedcomponent1.component.scss │ │ │ │ │ ├── sharedcomponent1.component.spec.ts │ │ │ │ │ ├── sharedcomponent1.component.ts │ │ │ ├── directives │ │ │ │ ├── directive1.ts │ │ │ │ ├── directive1.spec.ts │ │ │ ├── generated //generated from openApi │ │ │ │ ├── models │ │ │ │ ├── services │ │ │ ├── shared.module.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.state.ts │ │ ├── app.reducers.ts │ │ ├── app.reducers.spec.ts │ │ ├── app.selectors.ts │ │ ├── app.selectors.spec.ts │ ├── assets │ │ │ ├── i18n │ │ │ ├── fonts │ │ │ ├── images │ │ │ ├── scss │ │ │ ├── yamls │ ├── environments │ │ │ ├── dev │ │ │ ├── prod
Subcomponents
Usually subcomponents have a single input for the viewmodel called vm. This viewmodel is a member of the components viewmodel and is build via selectors. If a subcomponent has also a subsubcomponent, the viewmodel of the subcomponent has a member containing the viewmodel of the subsubcomponent.
Dialogs
There are two kinds of dialogs:
-
dialogs only used in one page
-
dialogs used in multiple pages
Both dialog types have in common that:
-
they are opened in effects
-
the result is handled in the effects and is converted into an action
-
they have a viewmodel
-
they are opened with
PortalDialogService.openDialog(…) -
they are communicating with the code that opened the dialog by implementing the interface
DialogResult -
Implementing
DialogButtonClickedcan help setting the dialog result before the dialog closes
Dialogs used in one page
The components used for dialogs are subcomponents of the page they are used in. Theses subcomponents are placed in a subfolder called 'dialogs'. They do not have a separate section in the store because they are sharing the data of the page they are belonging to. The viewmodel can be passed via an input which is called 'vm'. The viewmodel is read from the store via a viewmodel selector in the effects or is build with data from the action which is triggering the opening of the dialog.
The dialogs can also dispatch actions but only if they are not used to react on the closing of the dialog. Reacting to the closing is always done via the Observable returned by openDialog(…)
Dialogs used in multiple pages
The structure and inner workings of a dialog that is used in many pages is similar to the structure of a page. They have their own section in the store, own effects, own selectors,… Dialogs that are used by multiple pages are always dispatching an action that is informing about the opening of the dialog in the constructor of the dialog component. The viewmodel is read from the store inside of the component via a selector like it is done for a page.
OneCX utilities
OneCX ships three libraries that support these guidelines. @onecx/ngrx-integration-interface, which brings OneCX platform state into the store, is covered in NgRx Setup. The other two are described below.
Helpers
Library: @onecx/ngrx-accelerator
@onecx/ngrx-accelerator provides small helpers for the recurring NgRx patterns in OneCX apps:
-
URI parameters. Helpers to keep route query parameters in sync with the store (
createQueryParamsEffect,filterOutQueryParamsHaveNotChanged,filterOutOnlyQueryParamsChanged) and to narrow router-store navigation to a given component (filterForNavigatedTo). The query-parameter helpers are marked deprecated in favour of fragment-parameter variants, but those variants are not yet exported from the@onecx/ngrx-acceleratorpackage entry point, so the query-parameter helpers are the ones you can import today. -
Selectors.
createChildSelectorsderives a set ofselect…selectors for the top-level keys of a nested feature state, so each component selects only its own slice. -
Local-storage rehydration.
lazyLoadingMergeReducerandcreateNestedKeyConfigurationhelp rehydrate only the slices you have persisted, rather than overwriting the whole store.
| The full helper API and usage examples are documented with the library. |
Enforcing the guidelines
Library: @onecx/ngrx-linter-rules
@onecx/ngrx-linter-rules is an ESLint plugin that encodes the guidelines on this page as static checks. Register the plugin and its recommended rules so that, for example, HTTP calls only run inside effects and effects never dispatch to the store directly. The exported configs.recommended uses the legacy .eslintrc shape, so in an ESLint flat config register the plugin object and reuse its rule map:
const ngrxLinterRules = require('@onecx/ngrx-linter-rules');
module.exports = [
{
plugins: {
'@onecx/ngrx-linter-rules': ngrxLinterRules.plugin,
},
rules: {
...ngrxLinterRules.configs.recommended.rules,
},
},
];
For the legacy .eslintrc setup and for overriding rule severities, see the ngrx-linter-rules library documentation.
Further information about NgRx
For a further introduction, please checkout the NgRX documentation.
Furthermore, the following video tutorial might be helpful.