├── src
├── assets
│ └── .gitkeep
├── favicon.ico
├── styles.css
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── typings.d.ts
├── tsconfig.app.json
├── index.html
├── tsconfig.spec.json
├── main.ts
├── test.ts
├── polyfills.ts
└── app
│ └── app.module.ts
├── e2e
├── app.po.ts
├── tsconfig.e2e.json
└── app.e2e-spec.ts
├── .editorconfig
├── tsconfig.json
├── .gitignore
├── protractor.conf.js
├── karma.conf.js
├── .angular-cli.json
├── package.json
├── README.md
└── tslint.json
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nrwl/ngrx_race_condition/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "module": "es2015",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 |
3 | describe('ngrxraceconditions App', () => {
4 | let page: AppPage;
5 |
6 | beforeEach(() => {
7 | page = new AppPage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to app!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ngrxraceconditions
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "node"
11 | ]
12 | },
13 | "files": [
14 | "test.ts"
15 | ],
16 | "include": [
17 | "**/*.spec.ts",
18 | "**/*.d.ts"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.log(err));
13 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es5",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | testem.log
34 | /typings
35 |
36 | # e2e
37 | /e2e/*.js
38 | /e2e/*.map
39 |
40 | # System Files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 | angularCli: {
23 | environment: 'dev'
24 | },
25 | reporters: ['progress', 'kjhtml'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['Chrome'],
31 | singleRun: false
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare const __karma__: any;
17 | declare const require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting()
26 | );
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().map(context);
31 | // Finally, start Karma to run the tests.
32 | __karma__.start();
33 |
--------------------------------------------------------------------------------
/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "ngrxraceconditions"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "app",
21 | "styles": [
22 | "styles.css"
23 | ],
24 | "scripts": [],
25 | "environmentSource": "environments/environment.ts",
26 | "environments": {
27 | "dev": "environments/environment.ts",
28 | "prod": "environments/environment.prod.ts"
29 | }
30 | }
31 | ],
32 | "e2e": {
33 | "protractor": {
34 | "config": "./protractor.conf.js"
35 | }
36 | },
37 | "lint": [
38 | {
39 | "project": "src/tsconfig.app.json"
40 | },
41 | {
42 | "project": "src/tsconfig.spec.json"
43 | },
44 | {
45 | "project": "e2e/tsconfig.e2e.json"
46 | }
47 | ],
48 | "test": {
49 | "karma": {
50 | "config": "./karma.conf.js"
51 | }
52 | },
53 | "defaults": {
54 | "styleExt": "css",
55 | "component": {}
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngrxraceconditions",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng serve",
8 | "build": "ng build",
9 | "test": "ng test",
10 | "lint": "ng lint",
11 | "e2e": "ng e2e"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "^4.2.4",
16 | "@angular/common": "^4.2.4",
17 | "@angular/compiler": "^4.2.4",
18 | "@angular/core": "^4.2.4",
19 | "@angular/forms": "^4.2.4",
20 | "@angular/http": "^4.2.4",
21 | "@angular/platform-browser": "^4.2.4",
22 | "@angular/platform-browser-dynamic": "^4.2.4",
23 | "@angular/router": "^4.2.4",
24 | "core-js": "^2.4.1",
25 | "rxjs": "^5.4.2",
26 | "zone.js": "^0.8.14"
27 | },
28 | "devDependencies": {
29 | "@angular/cli": "1.4.0-rc.2",
30 | "@angular/compiler-cli": "^4.2.4",
31 | "@angular/language-service": "^4.2.4",
32 | "@types/jasmine": "~2.5.53",
33 | "@types/jasminewd2": "~2.0.2",
34 | "@types/node": "~6.0.60",
35 | "@ngrx/store": "^4.0.3",
36 | "@ngrx/effects": "^4.0.3",
37 | "@ngrx/store-devtools": "^4.0.0",
38 | "codelyzer": "~3.1.1",
39 | "jasmine-core": "~2.6.2",
40 | "jasmine-spec-reporter": "~4.1.0",
41 | "karma": "~1.7.0",
42 | "karma-chrome-launcher": "~2.1.1",
43 | "karma-cli": "~1.0.1",
44 | "karma-coverage-istanbul-reporter": "^1.2.1",
45 | "karma-jasmine": "~1.1.0",
46 | "karma-jasmine-html-reporter": "^0.2.2",
47 | "protractor": "~5.1.2",
48 | "ts-node": "~3.2.0",
49 | "tslint": "~5.3.2",
50 | "typescript": "~2.3.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular and NgRx: A Race Condition Illustration + An Easy Fix
2 |
3 | This is an Angular app using NgRx. It illustrates a race condition that occurs in many web applications. It also shows how easy it is to fix it when you use NgRx.
4 |
5 | It's important to understand that such race conditions are not Angular or NgRx specific. Any web application that talks to the backend can have them. The ease with which we can fix it, however, is due to the awesomeness of NgRx.
6 |
7 |
8 | ## Repro
9 |
10 | Check out the "first" commit and run `ng serve` . Do the following:
11 |
12 | * Wait for the data to load (takes 4 seconds)
13 | * Click on "Open"
14 | * Click on "Update Item" (it will take 4 seconds for the item to update),
15 | * Immediately click on "Back"
16 |
17 | If you have the redux dev tools open, you will see the following actions:
18 |
19 | * LOAD_ITEMS
20 | * ITEMS_LOADED
21 | * UPDATE_ITEM
22 | * LOAD_ITEMS
23 | * LOAD_ITEM
24 | * ITEM_LOADED
25 | * ITEMS_LOADED
26 |
27 | And the value of the item will flicker from the updated one to the old one. There is a bug!
28 |
29 | The second `LOAD_ITEMS` executes before `LOAD_ITEM` and captures the state of the server before the update is applied. The response for `LOAD_ITEMS` takes longer to propagate, and, as a result, its obsolete data overwrites the new updated version.
30 |
31 |
32 | ## Fix
33 |
34 | Every time we have a situation like this, we run multiple requests independently, whereas they should run them in order. In this case, all operations loading items must run in order.
35 |
36 | Check out the "second" commit and run `ng serve` . If you follow the same instructions, the order of actions will look like this:
37 |
38 | * LOAD_ITEMS
39 | * ITEMS_LOADED
40 | * UPDATE_ITEM
41 | * LOAD_ITEMS
42 | * LOAD_ITEM
43 | * ITEMS_LOADED
44 | * ITEM_LOADED
45 |
46 | We achieved that by applying both `LOAD_ITEMS` and `LOAD_ITEM` in the same effect. There are other ways to fix it, but of them boil down to: **Take 2 independent observables** and combine them using `concatMap`.
47 |
48 |
49 |
50 | ## Nrwl — Enterprise Angular Consulting.
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** Evergreen browsers require these. **/
41 | import 'core-js/es6/reflect';
42 | import 'core-js/es7/reflect';
43 |
44 |
45 | /**
46 | * Required to support Web Animations `@angular/animation`.
47 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
48 | **/
49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
50 |
51 |
52 |
53 | /***************************************************************************************************
54 | * Zone JS is required by Angular itself.
55 | */
56 | import 'zone.js/dist/zone'; // Included with Angular CLI.
57 |
58 |
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
64 | /**
65 | * Date, currency, decimal and percent pipes.
66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
67 | */
68 | // import 'intl'; // Run `npm install --save intl`.
69 | /**
70 | * Need to import at least one locale-data with intl.
71 | */
72 | // import 'intl/locale-data/jsonp/en';
73 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "eofline": true,
15 | "forin": true,
16 | "import-blacklist": [
17 | true,
18 | "rxjs"
19 | ],
20 | "import-spacing": true,
21 | "indent": [
22 | true,
23 | "spaces"
24 | ],
25 | "interface-over-type-literal": true,
26 | "label-position": true,
27 | "max-line-length": [
28 | true,
29 | 140
30 | ],
31 | "member-access": false,
32 | "member-ordering": [
33 | true,
34 | {
35 | "order": [
36 | "static-field",
37 | "instance-field",
38 | "static-method",
39 | "instance-method"
40 | ]
41 | }
42 | ],
43 | "no-arg": true,
44 | "no-bitwise": true,
45 | "no-console": [
46 | true,
47 | "debug",
48 | "info",
49 | "time",
50 | "timeEnd",
51 | "trace"
52 | ],
53 | "no-construct": true,
54 | "no-debugger": true,
55 | "no-duplicate-super": true,
56 | "no-empty": false,
57 | "no-empty-interface": true,
58 | "no-eval": true,
59 | "no-inferrable-types": [
60 | true,
61 | "ignore-params"
62 | ],
63 | "no-misused-new": true,
64 | "no-non-null-assertion": true,
65 | "no-shadowed-variable": true,
66 | "no-string-literal": false,
67 | "no-string-throw": true,
68 | "no-switch-case-fall-through": true,
69 | "no-trailing-whitespace": true,
70 | "no-unnecessary-initializer": true,
71 | "no-unused-expression": true,
72 | "no-use-before-declare": true,
73 | "no-var-keyword": true,
74 | "object-literal-sort-keys": false,
75 | "one-line": [
76 | true,
77 | "check-open-brace",
78 | "check-catch",
79 | "check-else",
80 | "check-whitespace"
81 | ],
82 | "prefer-const": true,
83 | "quotemark": [
84 | true,
85 | "single"
86 | ],
87 | "radix": true,
88 | "semicolon": [
89 | true,
90 | "always"
91 | ],
92 | "triple-equals": [
93 | true,
94 | "allow-null-check"
95 | ],
96 | "typedef-whitespace": [
97 | true,
98 | {
99 | "call-signature": "nospace",
100 | "index-signature": "nospace",
101 | "parameter": "nospace",
102 | "property-declaration": "nospace",
103 | "variable-declaration": "nospace"
104 | }
105 | ],
106 | "typeof-compare": true,
107 | "unified-signatures": true,
108 | "variable-name": false,
109 | "whitespace": [
110 | true,
111 | "check-branch",
112 | "check-decl",
113 | "check-operator",
114 | "check-separator",
115 | "check-type"
116 | ],
117 | "directive-selector": [
118 | true,
119 | "attribute",
120 | "app",
121 | "camelCase"
122 | ],
123 | "component-selector": [
124 | true,
125 | "element",
126 | "app",
127 | "kebab-case"
128 | ],
129 | "use-input-property-decorator": true,
130 | "use-output-property-decorator": true,
131 | "use-host-property-decorator": true,
132 | "no-input-rename": true,
133 | "no-output-rename": true,
134 | "use-life-cycle-interface": true,
135 | "use-pipe-transform-interface": true,
136 | "component-class-suffix": true,
137 | "directive-class-suffix": true,
138 | "no-access-missing-member": true,
139 | "templates-use-public": true,
140 | "invoke-injectable": true
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser';
2 | import {Component, Injectable, NgModule} from '@angular/core';
3 | import {ActivatedRoute, RouterModule} from '@angular/router';
4 | import {Store, StoreModule} from '@ngrx/store';
5 | import {Actions, Effect, EffectsModule} from '@ngrx/effects';
6 | import 'rxjs/add/operator/switchMap';
7 | import {of} from 'rxjs/observable/of';
8 | import 'rxjs/add/operator/map';
9 | import 'rxjs/add/operator/concatMap';
10 | import {StoreDevtoolsModule} from '@ngrx/store-devtools';
11 | import {timer} from 'rxjs/observable/timer';
12 | import 'rxjs/add/operator/filter';
13 |
14 | // components
15 | @Component({
16 | selector: 'app-root',
17 | template: `
18 |
19 | `
20 | })
21 | export class AppComponent {
22 | }
23 |
24 | @Component({
25 | selector: 'app-list',
26 | template: `
27 |
28 | {{i.id}}: {{i.value}}
Open
29 |
30 |
31 |
32 | `
33 | })
34 | export class ListComponent {
35 | items = this.store.select('items');
36 |
37 | constructor(private store: Store) {
38 | store.dispatch({type: 'LOAD_ITEMS'});
39 | }
40 |
41 | reload() {
42 | this.store.dispatch({type: 'LOAD_ITEMS'});
43 | }
44 | }
45 |
46 | @Component({
47 | selector: 'app-item',
48 | template: `
49 | id {{(item | async).id}}: {{(item | async).value}}
50 |
51 | Back
52 | `
53 | })
54 | export class ItemComponent {
55 | item = this.store.select('items').map(items => items[+this.route.snapshot.params.id]);
56 |
57 | constructor(private store: Store, private route: ActivatedRoute) {
58 | }
59 |
60 | reload() {
61 | this.store.dispatch({type: 'LOAD_ITEMS'});
62 | }
63 |
64 | update() {
65 | this.store.dispatch({type: 'UPDATE_ITEM', payload: {id: +this.route.snapshot.params.id}});
66 | }
67 | }
68 |
69 |
70 |
71 | // Simulating backend data
72 | // for convenience: the id is the index of an item in the array
73 | let backendData = [
74 | {id: 0, value: 'one'},
75 | {id: 1, value: 'two'}
76 | ];
77 |
78 | @Injectable()
79 | export class AppEffects {
80 | @Effect() loadAllOrOne = this.actions.filter((a:any) => a.type === 'LOAD_ITEMS' || a.type === 'LOAD_ITEM').concatMap((a:any) => {
81 | return a.type === 'LOAD_ITEM' ? this.handleLoadItem(a) : this.handleLoadItems(a);
82 | });
83 |
84 | @Effect() updateItem = this.actions.ofType('UPDATE_ITEM').concatMap((l: any) => {
85 | return timer(4000).map(() => {
86 | const newBackendData = [{...backendData[0]}, {...backendData[1]}];
87 | newBackendData[l.payload.id].value = 'UPDATED';
88 | backendData = newBackendData;
89 |
90 | return {
91 | type: 'LOAD_ITEM',
92 | payload: l.payload
93 | };
94 | });
95 | });
96 |
97 | constructor(private actions: Actions) {
98 | }
99 |
100 | private handleLoadItem(l: any): any {
101 | return of({
102 | type: 'ITEM_LOADED',
103 | payload: backendData[l.payload.id]
104 | });
105 | }
106 |
107 | private handleLoadItems(l: any): any {
108 | const copy = [{...backendData[0]}, {...backendData[1]}];
109 | return timer(4000).map(() => {
110 | return {
111 | type: 'ITEMS_LOADED',
112 | payload: copy
113 | };
114 | });
115 | }
116 | }
117 |
118 |
119 | export function items(state, action) {
120 | switch (action.type) {
121 | case 'ITEMS_LOADED': {
122 | return action.payload;
123 | }
124 | case 'ITEM_LOADED': {
125 | const index = action.payload.id;
126 | return [...state.slice(0, index), action.payload, ...state.slice(index + 1)];
127 | }
128 | default: {
129 | return state;
130 | }
131 | }
132 | }
133 |
134 | @NgModule({
135 | declarations: [
136 | AppComponent,
137 | ListComponent,
138 | ItemComponent
139 | ],
140 | imports: [
141 | BrowserModule,
142 | RouterModule.forRoot([
143 | {path: '', component: ListComponent},
144 | {path: 'items/:id', component: ItemComponent}
145 | ]),
146 | StoreModule.forRoot({
147 | items
148 | }),
149 | EffectsModule.forRoot([AppEffects]),
150 | StoreDevtoolsModule.instrument()
151 | ],
152 | providers: [AppEffects],
153 | bootstrap: [AppComponent]
154 | })
155 | export class AppModule {
156 | }
157 |
--------------------------------------------------------------------------------