SlideShare a Scribd company logo
NGRXNGRX
THERE IS A REDUCER IN MY SOUPTHERE IS A REDUCER IN MY SOUP
1
About me
Google Developer Expert
Telerik Developer Expert
Digital McKinsey
@chris_noring
2
NGRX IS:NGRX IS:
An Angular implementation of Redux
3 . 1
Why do we care about Redux?
If you experience the following symtoms you might
need Redux:
A feeling that the state is spread out
There are issues with updating state
Some things keep changing the state but you don't
know who or what
Un an explainable itch
3 . 2
Solution: a single source of truth with reducers
guarding state change. Also enhanced predictability
with immutable data structures
3 . 3
CORE CONCEPTSCORE CONCEPTS
3 . 4
Store, our data store
Reducer, a function that takes state + action and
produces a new state
Selector, selects a slice of state
Action, an intention of state change
Action creator, produces an intention that may
include data
3 . 5
Typical Store content, just an object
{
counter: 'a value',
jedis: [{ id: 1, name: 'Yoda' }],
selectedJedi: { id: 1, name: 'Yoda' }
}
3 . 6
REDUCERREDUCER
NEW STATE = STATE + ACTIONNEW STATE = STATE + ACTION
3 . 7
Let's take some reducing examples:
BANANA + MILK + ICE CREAM =BANANA + MILK + ICE CREAM =
MILKSHAKEMILKSHAKE
PIZZA + HAMBURGER + ICECREAM =PIZZA + HAMBURGER + ICECREAM =
STOMACH ACHESTOMACH ACHE
3 . 8
Mathematical function, immutable, just a calculation
Immutable = Predictability
//mutating
var sum = 3;
function add(a) { sum += a; return sum; }
add(5); // 8
add(5); // 13
// immutable
function computeSum(a,b) { return a + b; }
computeSum(1,1); // 2
computeSum(1,1); // 2
3 . 9
A reducer looks like the following:
function reducer(state, action) { /* implementation */ }
state, previous/initial state
action = {
type: 'my intent, e.g ADD_ITEM',
payload: { /* some kind of object */ }
}
3 . 10
state + 1, instead of state +=1, immutable
function counterReducer(state = 0, action) {
switch(action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
}
3 . 11
Usage
let initialState = 0;
let state = counterReducer(initialState,{ type: 'INCREMENT' })
// 1
state = counterReducer(state, { type: 'INCREMENT' })
// 2
function counterReducer(state = 0, action) {
switch(action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
}
3 . 12
A list reducer
function jediReducer(state = [], action) {
switch(action.type) {
case 'ADD_JEDI':
return [ ...state, action.payload];
case 'REMOVE_JEDI':
return state.filter(
jedi => jedi.id !== action.payload.id);
default:
return state;
}
}
3 . 13
Usage, list reducer
let state = jediReducer([], {
type: 'ADD_JEDI',
payload: { id: 1, name: 'Yoda' }
});
// [{ id: 1, name: 'Yoda' }]
state = jediReducer(state, {
type: 'REMOVE_JEDI',
payload: { id: 1 }
});
// []
3 . 14
An object reducer
Build a new object based on old + change with
...spread
let initialState = {
loading: false,
data: [],
error: void 0
};
function productsReducer(state = initialState, action) {
switch(action.type) {
case 'FETCHING_PRODUCTS':
return { ...state, loading: true };
case 'FETCHED_PRODUCTS':
return { ...state, data: action.payload, loading: false
case 'FETCHED_PRODUCTS_ERROR':
return { ...state, error: action.payload, loading: false
}
}
3 . 15
Object reducer, usage
Handling an AJAX case, can show spinner when,
loading = true
let state = productsReducer(initialState, {
type: 'FETCHING_PRODUCTS'
});
try {
let products = await getProducts(); // from an endpoint;
state = productsReducer(state, {
type: 'FETCHED_PRODUCTS',
payload: products
});
} catch (error) {
state = productsReducer(state, {
type: 'FETCHED_PRODUCTS_ERROR',
payload: error
});
}
3 . 16
A simple store
class Store {
constructor(initialState) { this.state = initialState; }
dispatch(action) {
this.state = calc(this.state, action);
}
calc(state, action) {
return {
counter: counterReducer(state.counter, action),
jedis: jediReducer(state.jedis, action)
}
}
}
3 . 17
Usage, store
let store = new Store({ counter: 0, jedis: [] });
store.dispatch({ type: 'INCREMENT' });
// { counter: 1, jedis: [] }
store.dispatch({
type: 'ADD_JEDI',
payload: { id: 1, name: 'Yoda' }
});
// { counter: 1, jedis: [{ id: 1, name: 'Yoda' }] }
store.dispatch({
type: 'REMOVE_JEDI',
payload: { id: 1 }
});
// { counter: 1, jedis: [] }
3 . 18
Action, an object with a property 'type' and 'payload'
'type' = intent
'payload' = the change
{ type: 'ADD_JEDI', payload: { id: 1, name: 'Yoda' } }
3 . 19
Action creator, function that creates action
const addJedi = (id, name) =>
({ type: 'ADD_JEDI', payload: { id, name } });
addJedi(1, 'Yoda');
// { type:'ADD_JEDI', payload: { id: 1, name: 'Yoda' } }
//usage
store.dispatch(addJedi(1, 'Yoda'));
3 . 20
Selector, slice of state
class Store {
constructor(initialState) { ... }
dispatch(action) { ... }
calc(state, action) { ... }
select(fn) {
return fn(state);
}
}
3 . 21
Selector, definitions
const getCounter = (state) => state.counter;
const getJedis = (state) => state.jedis;
3 . 22
NGRXNGRX
OVERVIEW OF LIBRARIESOVERVIEW OF LIBRARIES
4 . 1
@ngrx/store, the store
@ngrx/store-devtools, a debug tool that helps you
track dispatched actions
@ngrx/router-store, lets you put the routing state in
the store
@ngrx/effects, handles side effects
@ngrx/entites, handles records
@ngrx/schematics
4 . 2
STORESTORE
WHERE THE STATE LIVESWHERE THE STATE LIVES
5 . 1
INSTALLATION AND SET UPINSTALLATION AND SET UP
npm install @ngrx/store --save
// file 'app.module.ts'
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';
@NgModule({
imports: {
StoreModule.forRoot({
counter: counterReducer
})
}
})
export class AppModule {}
5 . 2
SHOW DATA FROM STORESHOW DATA FROM STORE
// app-state.ts
export interface AppState {
counter: number;
}
// some.component.ts
@Component({
template: ` {{ counter$ | async }} `
})
export class SomeComponent {
counter$;
constructor(this store:Store<AppState>) {
this.counter$ = this.store.select('counter');
}
}
5 . 3
SHOW DATA FROM STORE, SELECTORSHOW DATA FROM STORE, SELECTOR
FUNCTIONFUNCTION
// app-state.ts
export interface AppState {
counter: number;
}
// some.component.ts
@Component({
template: ` {{ counter$ | async }} `
})
export class SomeComponent {
counter$;
constructor(this store:Store<AppState>) {
this.counter$ = this.store
.select( state => state.counter);
}
}
5 . 4
DISPATCH DATADISPATCH DATA
@Component({
template: `
{{ counter$ | async }}
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
`
})
export class SomeComponent {
counter$;
constructor(this store:Store<AppState>) {
this.counter$ = this.store.select('counter');
}
increment() { this.store.dispatch({ type: 'INCREMENT' }); }
decrement() { this.store.dispatch({ type: 'DECREMENT' }); }
}
5 . 5
DISPATCH DATA, WITH PAYLOAD,DISPATCH DATA, WITH PAYLOAD,
TEMPLATETEMPLATE
@Component({
template: `
<input [(ngModel)]="newProduct" />
<div *ngFor="let product of products$ | async">
{{ product.name }}
<button (click)="remove(product.id)">Remove</button>
</div>
<button (click)="add()">Add</button>
`
})
5 . 6
CLASS BODYCLASS BODY
export class SomeComponent {
products$, id = 0;
constructor(this store:Store<AppState>) {
this.counter$ = this.store.select('products');
}
remove(id) { this.state.dispatch({
type: 'REMOVE_PRODUCT', payload: { id } })
}
add() {
this.state.dispatch({
type: 'ADD_PRODUCT',
payload: { id : this.id++, this.name: newProduct }
})
}
}
5 . 7
When our app grows, we can't have all the state in
StoreModule.forRoot({})
Solution is using StoreModule.forFeature()
Let's also find a way to organize files
5 . 8
Set up store in a feature module,
StoreModule.forFeature('feature',{})
interface CombinedState {
list: Product[];
item: Product;
};
const combinedReducers: ActionReducerMap<CombinedState> = {
list: listReducer,
item: itemReducer
};
@NgModule({
imports: [
StoreModule.forFeature<CombinedState, Action>(
'products',
combinedReducers
)]
});
5 . 9
Organizing your files, Domain approach
/feature
feature.component.ts
feature.selector.ts
feature.actions.ts
feature.reducer.ts
5 . 10
Organizing your files, Ruby on Rails approach
/feature
feature.component.ts
/feature2
feature2.component.ts
/reducers
feature.reducer.ts
feature2.reducer.ts
index.ts
/selectors
feature.selector.ts
feature2.selector.ts
index.ts
/actions
feature.actions.ts
feature2.actions.ts
5 . 11
Whatever approach you go for, consistency is key
5 . 12
STORE DEVTOOLSSTORE DEVTOOLS
DEBUG LIKE A PRODEBUG LIKE A PRO
6 . 1
INSTALLATION AND SET UPINSTALLATION AND SET UP
Install lib on NPM and download chrome extension on
http://extension.remotedev.io/
6 . 2
npm install @ngrx/store-devtools
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreDevtoolsModule.instrument({
maxAge: 25 // Retains last 25 states
}
)]
})
6 . 3
What can we do with it?
See dispatched actions
Undo dispatched actions
Use time travel debugging and move through time,
back and forth with a gauge
6 . 4
Initial view, we see actions as well as different tabs
6 . 5
Here we can see the detail of an action, what type etc.
6 . 6
It records all of our actions, here multiple dispatched
actions
6 . 7
Here we are undoing/skipping an action, store is
recalculated
6 . 8
This gauge enables us to travel through time, back and
forth
6 . 9
ROUTER STOREROUTER STORE
ENABLING US TO PUT THE ROUTERENABLING US TO PUT THE ROUTER
STATE IN THE STORESTATE IN THE STORE
7 . 1
We want to accomplish the following:
Save the route state to our store
Customize whats gets saved
down
7 . 2
INSTALLATION AND SET UPINSTALLATION AND SET UP
npm install @ngrx/router-store
import { StoreRouterConnectingModule } from '@ngrx/router-stor
@NgModule({
imports: [
StoreModule.forRoot({
router: routerReducer // this is where our route state g
}),
StoreRouterConnectingModule.forRoot({
stateKey: 'router' // name of reducer key
})
]
})
7 . 3
We can listen to this 'router' state like any other state
@Component({
template: ``
})
export class SomeComponent {
constructor(private state: State<AppState>) {
// updates every time we route
this.state.select('router')
.subscribe(data => console.log(data));
}
}
7 . 4
Might be a bit hard to read
7 . 5
LET'S BUILD OUR OWN ROUTER STATELET'S BUILD OUR OWN ROUTER STATE
7 . 6
The following is of interest:
The url
The router
parameters
The query parameters
7 . 7
Define a serializer, that saves url, queryParams and
router params
interface MyState {
url: string;
queryParams;
params;
}
export class MySerializer implements
RouterStateSerializer<MyState> {
serialize(routerState: RouterStateSnapshot): MyState {
console.log('serializer');
console.log('complete router state', routerState);
const { url, root: { queryParams, firstChild: { params } }
return { url, queryParams, params };
}
}
7 . 8
Provide this as the 'new' serializer
@NgModule({
providers: [{
provide: RouterStateSerializer,
useClass: MySerializer
}]
})
7 . 9
This is what the router state looks like now, only saves
exactly what we want
7 . 10
EFFECTSEFFECTS
HANDLING SIDE EFFECTSHANDLING SIDE EFFECTS
8 . 1
Objective: We want to ensure we can carry out things
like accessing resources over the network.
8 . 2
INSTALLATION AND SETUPINSTALLATION AND SETUP
npm install @ngrx/effects
import { EffectsModule } from '@ngrx/effects';
@NgModule({
EffectsModule.forRoot([ ... my effects classes ])
})
8 . 3
What kind of behaviour do we want?
Set a loading flag, show spinner
Do AJAX call
Show fetched data or error
Set loading flag to false, hide
spinner
8 . 4
NGRX approach
try {
store.dispatch({ type: 'FETCHING_DATA' })
// state: { loading: true }
const data = await getData(); // async operation
store.dispatch({ type: 'FETCHED_DATA', payload: data });
// state: { loading: false, data: {/* data from endpoint */}
} catch (error) {
store.dispatch({
type: 'FETCHED_DATA_ERROR',
payload: error
});
}
8 . 5
My first effect
@Injectable()
export class ProductEffects {
@Effect()
products$: Observable<Action> = this.actions$.pipe(
ofType(FETCHING_PRODUCTS),
switchMap(
// ensure we dispatch an action the last thing we do
action => of({ type: "SOME_ACTION" })
)
);
constructor(private actions$: Actions<Action>, private http:
console.log("product effects init");
}
}
8 . 6
My first effect - calling HTTP
@Injectable()
export class ProductEffects {
@Effect()
products$: Observable<Action> = this.actions$.pipe(
ofType(FETCHING_PRODUCTS), // listen to this action
switchMap(action =>
this.http
.get("data/products.json")
.pipe(
delay(3000),
map(fetchProductsSuccessfully), // success
catchError(err => of(fetchError(err))) // error
)
)
);
8 . 7
ENTITESENTITES
REDUCE THAT BORING BOILER PLATEREDUCE THAT BORING BOILER PLATE
9 . 1
Install and set up
npm install @ngrx/entity
import {
EntityState,
createEntityAdapter,
EntityAdapter } from "@ngrx/entity";
// set up the adapter
const adapter: EntityAdapter<User> =
createEntityAdapter<User>();
// set up initial state
const initial = adapter.getInitialState({
ids: [],
entities: {}
});
9 . 2
Install and set up
/*
use the adapter methods
for specific cases like adapter.addOne()
*/
function userReducer(
state = initial,
action: ActionPayload<User>
) {
switch (action.type) {
case "ADD_USER":
return adapter.addOne(action.payload, state);
default:
return state;
}
}
9 . 3
What else can it do for us?
addOne: Add one entity to the collection
addMany: Add multiple entities to the collection
addAll: Replace current collection with provided
collection
removeOne: Remove one entity from the collection
removeMany: Remove multiple entities from the
collection
removeAll: Clear entity collection
updateOne: Update one entity in the collection
updateMany: Update multiple entities in the
collection 9 . 4
Further reading:
https://github.com/ngrx/platform/tree/master/docs/ent
9 . 5
SCHEMATICSSCHEMATICS
BE LAZY AND SCAFFOLD :)BE LAZY AND SCAFFOLD :)
10 . 1
It is a scaffolding library that helps us scaffold out
NGRX features
10 . 2
Schematics can help us scaffold the following:
Action
Container
Effect
Entity
Feature
Reducer
Store
10 . 3
Install and set up
// install schematics and prerequisits
npm install @ngrx/schematics --save-dev
npm install @ngrx/{store,effects,entity,store-devtools} --save
// we might need this one as well
npm install --save @angular/cli@latest
10 . 4
Scaffold, initial state set up, forRoot({}) and
instrument()
ng generate store State --root --module app.module.ts --collec
// result
@NgModule({
declarations: [ ... ],
imports: [
BrowserModule,
StoreModule.forRoot(reducers, { metaReducers }),
!environment.production ? StoreDevtoolsModule.instrument()
],
bootstrap: [AppComponent]
})
export class AppModule { }
10 . 5
Scaffold, setting up effects for the App
ng generate effect App --root --module app.module.ts --collect
@NgModule({
declarations: [
AppComponent
],
imports: [
...
EffectsModule.forRoot([AppEffects])
],
providers: [],
bootstrap: [ ... ]
})
export class AppModule { }
10 . 6
Scaffold, create Action, generates action and test
ng generate action User --spec
export enum UserActionTypes {
UserAction = '[User] Action'
}
export class User implements Action {
readonly type = UserActionTypes.UserAction;
}
export type UserActions = User;
10 . 7
Scaffold, create a component, with store injected
ng generate container TodoList
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
constructor(private store: Store<any>) { }
ngOnInit() {
}
}
10 . 8
DO IT YOURSELFDO IT YOURSELF
BUILD YOUR OWN NGRXBUILD YOUR OWN NGRX
11 . 1
What requirements do we have?
Should be possible to dispatch actions
State should update when we dispatch
It should be possible to subscribe to a slice of
state
We should support side effects
11 . 2
BEHAVIOURSUBJECTBEHAVIOURSUBJECT
SUPPORTS A STATE, CAN BESUPPORTS A STATE, CAN BE
SUBSCRIBED TOSUBSCRIBED TO
11 . 3
BehaviorSubject
// ctor value = inital value
let subject = new BehaviorSubject({ value: 1 })
subject.subscribe(data => console.log('data', data));
// { value: 1 }
// { prop: 2}
subject.next({ prop: 2 });
11 . 4
Merging states with .scan()
let subject = new BehaviorSubject({ value: 1 }) // {} inital
subject
.scan((acc, value) =>({ ...acc, ...value }))
.subscribe(data => console.log('data', data));
subject.next({ prop: 2 }); // { value: 1, prop: 2 }
11 . 5
Implementing the store
class Store extends BehaviorSubject {
constructor(initialState = {}) {
super(initialState);
this.listenerMap = {};
this.dispatcher = new Subject();
this.dispatcher
.scan((acc, value) =>({ ...acc, ...value }))
.subscribe(state => super.next(state));
}
dispatch(newState) {
this.dispatcher.next(newState);
}
}
11 . 6
Usage, store
store = new Store();
store.subscribe(data => console.log('state', data));
store.dispatch({ val: 1 });
store.dispatch({ prop: 'string' });
// { val: 1, prop: 'string' }
11 . 7
What about slice of state?
class Store extends BehaviorSubject {
constructor(initialState = {}) {
...
}
dispatch(newState) {
this.dispatcher.next(newState);
}
select(slice) { return this.map[slice] }
selectWithFn(fn) { return this.map(fn) }
}
11 . 8
We need to improve the core implementation, enter
storeCalc()
const storeCalc = (state, action) => {
return {
counter: countReducer(state.counter, action),
products: productsReducer(state.products, action)
}
};
11 . 9
A retake on our dispatch(), old state, getValue() +
action = new state
dispatch(action) {
const newState = storeCalc(this.getValue(),action);
this.dispatcher.next(newState);
}
11 . 10
LETS' TALK ABOUT EFFECTSLETS' TALK ABOUT EFFECTS
11 . 11
We need to be able to signup to specific
actions
We need to be able to carry out side effects
11 . 12
First let's set up subscription in the store
class Store {
constructor() { ... }
dispatch() { ... }
select() { ... }
effect(listenToAction, listener) {
if(!this.listenerMap.hasOwnProperty(listenToAction)) {
this.listenerMap[listenToAction] = [];
}
this.listenerMap[listenToAction].push( listener );
}
}
11 . 13
Then ensure the effect happens in dispatch()
class Store {
constructor() { ... }
dispatch() {
const newState = storeCalc(this.getValue(),action);
this.dispatcher.next(newState);
// tell our listeners this action.type happened
if(this.listenerMap[action.type]) {
this.listenerMap[action.type].forEach(listener => {
listener(this.dispatch.bind(this),action);
});
}
}
select() { ... }
effect(listenToAction, listener) { ... }
}
11 . 14
use our new effect() method
let store = new Store();
store.effect('INCREMENT' ,async(dispatch, action) => {
// side effect
let products = await getProducts();
// side effect
let data = await getData();
// dispatch, if we want
dispatch({ type: 'INCREMENT' });
})
store.dispatch({ type: 'DECREMENT' });
11 . 15
SUMMARYSUMMARY
12 . 1
We learned how to:
Grasp the basics of Redux
NGRX building blocks
Use the store
Leverage the dev tools and its Redux plugin
Store our router state and transform it
How we handle side effect like AJAX calls
Remove boiler plate with Entity
How to be even lazier with the scaffold tool
Schematics
Upgrading ourselves to Ninja level by learning how
to implement NGRX
12 . 2
Further reading:
Free video course,
https://platform.ultimateangular.com/courses/ngrx-st
effects
Redux docs, https://redux.js.org/docs
Probably the best homepage on it, Brian Troncone,
https://gist.github.com/btroncone/a6e4347326749f93
12 . 3
Buy my book ( please :) ):
https://www.packtpub.com/web-
development/architecting-angular-applications-flux-
redux-ngrx
12 . 4
Thank you for listening
12 . 5

More Related Content

PDF
Getting Started with NgRx (Redux) Angular
PDF
Angular state Management-NgRx
PPTX
Ngrx: Redux in angular
PDF
RxJS Operators - Real World Use Cases (FULL VERSION)
PPTX
Angular 9
PDF
Introduction to RxJS
PPTX
React/Redux
PPTX
Angular Data Binding
Getting Started with NgRx (Redux) Angular
Angular state Management-NgRx
Ngrx: Redux in angular
RxJS Operators - Real World Use Cases (FULL VERSION)
Angular 9
Introduction to RxJS
React/Redux
Angular Data Binding

What's hot (20)

PPTX
React js programming concept
PPTX
[Final] ReactJS presentation
PDF
Understanding react hooks
PDF
Redux Toolkit - Quick Intro - 2022
PDF
An introduction to React.js
PPTX
Introduction to React JS for beginners
PDF
React js
PDF
Introduction to Redux
PDF
React new features and intro to Hooks
PDF
Angular - Chapter 7 - HTTP Services
PDF
React and redux
PPTX
React-JS Component Life-cycle Methods
PPTX
React workshop
PDF
Redux toolkit
PDF
React JS - Introduction
ODP
Introduction to ReactJS
PDF
An Introduction to Redux
PDF
Important React Hooks
React js programming concept
[Final] ReactJS presentation
Understanding react hooks
Redux Toolkit - Quick Intro - 2022
An introduction to React.js
Introduction to React JS for beginners
React js
Introduction to Redux
React new features and intro to Hooks
Angular - Chapter 7 - HTTP Services
React and redux
React-JS Component Life-cycle Methods
React workshop
Redux toolkit
React JS - Introduction
Introduction to ReactJS
An Introduction to Redux
Important React Hooks
Ad

Similar to Ngrx slides (20)

PDF
Redux with angular 2 - workshop 2016
PDF
Evan Schultz - Angular Summit - 2016
PDF
Introduction to Redux (for Angular and React devs)
PDF
Understanding redux
PDF
Evan Schultz - Angular Camp - ng2-redux
PDF
Redux State Management System A Comprehensive Review
PDF
Reactive.architecture.with.Angular
PDF
Architecture for scalable Angular applications
PPTX
Maintaining sanity in a large redux app
PPTX
Redux training
PDF
Redux Deep Dive - ReactFoo Pune 2018
PDF
Angular2 and Redux - up & running - Nir Kaufman - Codemotion Amsterdam 2016
PDF
Angular redux
PDF
redux and angular - up and running
PDF
The Road To Redux
PPTX
Redux Tech Talk
PDF
Introduction to ReactJS and Redux
PDF
Advanced redux
PDF
Redux data flow with angular
PDF
Egghead redux-cheat-sheet-3-2-1
Redux with angular 2 - workshop 2016
Evan Schultz - Angular Summit - 2016
Introduction to Redux (for Angular and React devs)
Understanding redux
Evan Schultz - Angular Camp - ng2-redux
Redux State Management System A Comprehensive Review
Reactive.architecture.with.Angular
Architecture for scalable Angular applications
Maintaining sanity in a large redux app
Redux training
Redux Deep Dive - ReactFoo Pune 2018
Angular2 and Redux - up & running - Nir Kaufman - Codemotion Amsterdam 2016
Angular redux
redux and angular - up and running
The Road To Redux
Redux Tech Talk
Introduction to ReactJS and Redux
Advanced redux
Redux data flow with angular
Egghead redux-cheat-sheet-3-2-1
Ad

More from Christoffer Noring (20)

PPTX
Azure signalR
PPTX
Game dev 101 part 3
PPTX
Game dev 101 part 2
PPTX
Game dev workshop
PPTX
Deploying your static web app to the Cloud
PPTX
IaaS with ARM templates for Azure
PPTX
Learning Svelte
PPTX
PDF
Angular Schematics
PDF
Design thinking
PDF
Keynote ijs
PDF
Vue fundamentasl with Testing and Vuex
PDF
PPTX
Angular mix chrisnoring
PDF
Nativescript angular
PDF
Graphql, REST and Apollo
PDF
Angular 2 introduction
PDF
Rxjs vienna
PPTX
Rxjs marble-testing
PDF
React lecture
Azure signalR
Game dev 101 part 3
Game dev 101 part 2
Game dev workshop
Deploying your static web app to the Cloud
IaaS with ARM templates for Azure
Learning Svelte
Angular Schematics
Design thinking
Keynote ijs
Vue fundamentasl with Testing and Vuex
Angular mix chrisnoring
Nativescript angular
Graphql, REST and Apollo
Angular 2 introduction
Rxjs vienna
Rxjs marble-testing
React lecture

Recently uploaded (20)

PDF
CIFDAQ's Market Wrap: Ethereum Leads, Bitcoin Lags, Institutions Shift
PDF
Sensors and Actuators in IoT Systems using pdf
PPTX
ABU RAUP TUGAS TIK kelas 8 hjhgjhgg.pptx
PDF
DevOps & Developer Experience Summer BBQ
PDF
ai-archetype-understanding-the-personality-of-agentic-ai.pdf
PPTX
breach-and-attack-simulation-cybersecurity-india-chennai-defenderrabbit-2025....
PDF
Modernizing your data center with Dell and AMD
PDF
Automating ArcGIS Content Discovery with FME: A Real World Use Case
PDF
Event Presentation Google Cloud Next Extended 2025
PDF
How Onsite IT Support Drives Business Efficiency, Security, and Growth.pdf
PDF
GamePlan Trading System Review: Professional Trader's Honest Take
PDF
Building High-Performance Oracle Teams: Strategic Staffing for Database Manag...
PPTX
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
PDF
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
PDF
Chapter 3 Spatial Domain Image Processing.pdf
PDF
CIFDAQ's Token Spotlight: SKY - A Forgotten Giant's Comeback?
PDF
NewMind AI Weekly Chronicles - August'25 Week I
PPTX
Comunidade Salesforce São Paulo - Desmistificando o Omnistudio (Vlocity)
PPTX
Belt and Road Supply Chain Finance Blockchain Solution
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
CIFDAQ's Market Wrap: Ethereum Leads, Bitcoin Lags, Institutions Shift
Sensors and Actuators in IoT Systems using pdf
ABU RAUP TUGAS TIK kelas 8 hjhgjhgg.pptx
DevOps & Developer Experience Summer BBQ
ai-archetype-understanding-the-personality-of-agentic-ai.pdf
breach-and-attack-simulation-cybersecurity-india-chennai-defenderrabbit-2025....
Modernizing your data center with Dell and AMD
Automating ArcGIS Content Discovery with FME: A Real World Use Case
Event Presentation Google Cloud Next Extended 2025
How Onsite IT Support Drives Business Efficiency, Security, and Growth.pdf
GamePlan Trading System Review: Professional Trader's Honest Take
Building High-Performance Oracle Teams: Strategic Staffing for Database Manag...
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
Chapter 3 Spatial Domain Image Processing.pdf
CIFDAQ's Token Spotlight: SKY - A Forgotten Giant's Comeback?
NewMind AI Weekly Chronicles - August'25 Week I
Comunidade Salesforce São Paulo - Desmistificando o Omnistudio (Vlocity)
Belt and Road Supply Chain Finance Blockchain Solution
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy

Ngrx slides

  • 1. NGRXNGRX THERE IS A REDUCER IN MY SOUPTHERE IS A REDUCER IN MY SOUP 1
  • 2. About me Google Developer Expert Telerik Developer Expert Digital McKinsey @chris_noring 2
  • 3. NGRX IS:NGRX IS: An Angular implementation of Redux 3 . 1
  • 4. Why do we care about Redux? If you experience the following symtoms you might need Redux: A feeling that the state is spread out There are issues with updating state Some things keep changing the state but you don't know who or what Un an explainable itch 3 . 2
  • 5. Solution: a single source of truth with reducers guarding state change. Also enhanced predictability with immutable data structures 3 . 3
  • 7. Store, our data store Reducer, a function that takes state + action and produces a new state Selector, selects a slice of state Action, an intention of state change Action creator, produces an intention that may include data 3 . 5
  • 8. Typical Store content, just an object { counter: 'a value', jedis: [{ id: 1, name: 'Yoda' }], selectedJedi: { id: 1, name: 'Yoda' } } 3 . 6
  • 9. REDUCERREDUCER NEW STATE = STATE + ACTIONNEW STATE = STATE + ACTION 3 . 7
  • 10. Let's take some reducing examples: BANANA + MILK + ICE CREAM =BANANA + MILK + ICE CREAM = MILKSHAKEMILKSHAKE PIZZA + HAMBURGER + ICECREAM =PIZZA + HAMBURGER + ICECREAM = STOMACH ACHESTOMACH ACHE 3 . 8
  • 11. Mathematical function, immutable, just a calculation Immutable = Predictability //mutating var sum = 3; function add(a) { sum += a; return sum; } add(5); // 8 add(5); // 13 // immutable function computeSum(a,b) { return a + b; } computeSum(1,1); // 2 computeSum(1,1); // 2 3 . 9
  • 12. A reducer looks like the following: function reducer(state, action) { /* implementation */ } state, previous/initial state action = { type: 'my intent, e.g ADD_ITEM', payload: { /* some kind of object */ } } 3 . 10
  • 13. state + 1, instead of state +=1, immutable function counterReducer(state = 0, action) { switch(action.type) { case 'INCREMENT': return state + 1; default: return state; } } 3 . 11
  • 14. Usage let initialState = 0; let state = counterReducer(initialState,{ type: 'INCREMENT' }) // 1 state = counterReducer(state, { type: 'INCREMENT' }) // 2 function counterReducer(state = 0, action) { switch(action.type) { case 'INCREMENT': return state + 1; default: return state; } } 3 . 12
  • 15. A list reducer function jediReducer(state = [], action) { switch(action.type) { case 'ADD_JEDI': return [ ...state, action.payload]; case 'REMOVE_JEDI': return state.filter( jedi => jedi.id !== action.payload.id); default: return state; } } 3 . 13
  • 16. Usage, list reducer let state = jediReducer([], { type: 'ADD_JEDI', payload: { id: 1, name: 'Yoda' } }); // [{ id: 1, name: 'Yoda' }] state = jediReducer(state, { type: 'REMOVE_JEDI', payload: { id: 1 } }); // [] 3 . 14
  • 17. An object reducer Build a new object based on old + change with ...spread let initialState = { loading: false, data: [], error: void 0 }; function productsReducer(state = initialState, action) { switch(action.type) { case 'FETCHING_PRODUCTS': return { ...state, loading: true }; case 'FETCHED_PRODUCTS': return { ...state, data: action.payload, loading: false case 'FETCHED_PRODUCTS_ERROR': return { ...state, error: action.payload, loading: false } } 3 . 15
  • 18. Object reducer, usage Handling an AJAX case, can show spinner when, loading = true let state = productsReducer(initialState, { type: 'FETCHING_PRODUCTS' }); try { let products = await getProducts(); // from an endpoint; state = productsReducer(state, { type: 'FETCHED_PRODUCTS', payload: products }); } catch (error) { state = productsReducer(state, { type: 'FETCHED_PRODUCTS_ERROR', payload: error }); } 3 . 16
  • 19. A simple store class Store { constructor(initialState) { this.state = initialState; } dispatch(action) { this.state = calc(this.state, action); } calc(state, action) { return { counter: counterReducer(state.counter, action), jedis: jediReducer(state.jedis, action) } } } 3 . 17
  • 20. Usage, store let store = new Store({ counter: 0, jedis: [] }); store.dispatch({ type: 'INCREMENT' }); // { counter: 1, jedis: [] } store.dispatch({ type: 'ADD_JEDI', payload: { id: 1, name: 'Yoda' } }); // { counter: 1, jedis: [{ id: 1, name: 'Yoda' }] } store.dispatch({ type: 'REMOVE_JEDI', payload: { id: 1 } }); // { counter: 1, jedis: [] } 3 . 18
  • 21. Action, an object with a property 'type' and 'payload' 'type' = intent 'payload' = the change { type: 'ADD_JEDI', payload: { id: 1, name: 'Yoda' } } 3 . 19
  • 22. Action creator, function that creates action const addJedi = (id, name) => ({ type: 'ADD_JEDI', payload: { id, name } }); addJedi(1, 'Yoda'); // { type:'ADD_JEDI', payload: { id: 1, name: 'Yoda' } } //usage store.dispatch(addJedi(1, 'Yoda')); 3 . 20
  • 23. Selector, slice of state class Store { constructor(initialState) { ... } dispatch(action) { ... } calc(state, action) { ... } select(fn) { return fn(state); } } 3 . 21
  • 24. Selector, definitions const getCounter = (state) => state.counter; const getJedis = (state) => state.jedis; 3 . 22
  • 26. @ngrx/store, the store @ngrx/store-devtools, a debug tool that helps you track dispatched actions @ngrx/router-store, lets you put the routing state in the store @ngrx/effects, handles side effects @ngrx/entites, handles records @ngrx/schematics 4 . 2
  • 27. STORESTORE WHERE THE STATE LIVESWHERE THE STATE LIVES 5 . 1
  • 28. INSTALLATION AND SET UPINSTALLATION AND SET UP npm install @ngrx/store --save // file 'app.module.ts' import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter.reducer'; @NgModule({ imports: { StoreModule.forRoot({ counter: counterReducer }) } }) export class AppModule {} 5 . 2
  • 29. SHOW DATA FROM STORESHOW DATA FROM STORE // app-state.ts export interface AppState { counter: number; } // some.component.ts @Component({ template: ` {{ counter$ | async }} ` }) export class SomeComponent { counter$; constructor(this store:Store<AppState>) { this.counter$ = this.store.select('counter'); } } 5 . 3
  • 30. SHOW DATA FROM STORE, SELECTORSHOW DATA FROM STORE, SELECTOR FUNCTIONFUNCTION // app-state.ts export interface AppState { counter: number; } // some.component.ts @Component({ template: ` {{ counter$ | async }} ` }) export class SomeComponent { counter$; constructor(this store:Store<AppState>) { this.counter$ = this.store .select( state => state.counter); } } 5 . 4
  • 31. DISPATCH DATADISPATCH DATA @Component({ template: ` {{ counter$ | async }} <button (click)="increment()">Increment</button> <button (click)="decrement()">Decrement</button> ` }) export class SomeComponent { counter$; constructor(this store:Store<AppState>) { this.counter$ = this.store.select('counter'); } increment() { this.store.dispatch({ type: 'INCREMENT' }); } decrement() { this.store.dispatch({ type: 'DECREMENT' }); } } 5 . 5
  • 32. DISPATCH DATA, WITH PAYLOAD,DISPATCH DATA, WITH PAYLOAD, TEMPLATETEMPLATE @Component({ template: ` <input [(ngModel)]="newProduct" /> <div *ngFor="let product of products$ | async"> {{ product.name }} <button (click)="remove(product.id)">Remove</button> </div> <button (click)="add()">Add</button> ` }) 5 . 6
  • 33. CLASS BODYCLASS BODY export class SomeComponent { products$, id = 0; constructor(this store:Store<AppState>) { this.counter$ = this.store.select('products'); } remove(id) { this.state.dispatch({ type: 'REMOVE_PRODUCT', payload: { id } }) } add() { this.state.dispatch({ type: 'ADD_PRODUCT', payload: { id : this.id++, this.name: newProduct } }) } } 5 . 7
  • 34. When our app grows, we can't have all the state in StoreModule.forRoot({}) Solution is using StoreModule.forFeature() Let's also find a way to organize files 5 . 8
  • 35. Set up store in a feature module, StoreModule.forFeature('feature',{}) interface CombinedState { list: Product[]; item: Product; }; const combinedReducers: ActionReducerMap<CombinedState> = { list: listReducer, item: itemReducer }; @NgModule({ imports: [ StoreModule.forFeature<CombinedState, Action>( 'products', combinedReducers )] }); 5 . 9
  • 36. Organizing your files, Domain approach /feature feature.component.ts feature.selector.ts feature.actions.ts feature.reducer.ts 5 . 10
  • 37. Organizing your files, Ruby on Rails approach /feature feature.component.ts /feature2 feature2.component.ts /reducers feature.reducer.ts feature2.reducer.ts index.ts /selectors feature.selector.ts feature2.selector.ts index.ts /actions feature.actions.ts feature2.actions.ts 5 . 11
  • 38. Whatever approach you go for, consistency is key 5 . 12
  • 39. STORE DEVTOOLSSTORE DEVTOOLS DEBUG LIKE A PRODEBUG LIKE A PRO 6 . 1
  • 40. INSTALLATION AND SET UPINSTALLATION AND SET UP Install lib on NPM and download chrome extension on http://extension.remotedev.io/ 6 . 2
  • 41. npm install @ngrx/store-devtools import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ StoreDevtoolsModule.instrument({ maxAge: 25 // Retains last 25 states } )] }) 6 . 3
  • 42. What can we do with it? See dispatched actions Undo dispatched actions Use time travel debugging and move through time, back and forth with a gauge 6 . 4
  • 43. Initial view, we see actions as well as different tabs 6 . 5
  • 44. Here we can see the detail of an action, what type etc. 6 . 6
  • 45. It records all of our actions, here multiple dispatched actions 6 . 7
  • 46. Here we are undoing/skipping an action, store is recalculated 6 . 8
  • 47. This gauge enables us to travel through time, back and forth 6 . 9
  • 48. ROUTER STOREROUTER STORE ENABLING US TO PUT THE ROUTERENABLING US TO PUT THE ROUTER STATE IN THE STORESTATE IN THE STORE 7 . 1
  • 49. We want to accomplish the following: Save the route state to our store Customize whats gets saved down 7 . 2
  • 50. INSTALLATION AND SET UPINSTALLATION AND SET UP npm install @ngrx/router-store import { StoreRouterConnectingModule } from '@ngrx/router-stor @NgModule({ imports: [ StoreModule.forRoot({ router: routerReducer // this is where our route state g }), StoreRouterConnectingModule.forRoot({ stateKey: 'router' // name of reducer key }) ] }) 7 . 3
  • 51. We can listen to this 'router' state like any other state @Component({ template: `` }) export class SomeComponent { constructor(private state: State<AppState>) { // updates every time we route this.state.select('router') .subscribe(data => console.log(data)); } } 7 . 4
  • 52. Might be a bit hard to read 7 . 5
  • 53. LET'S BUILD OUR OWN ROUTER STATELET'S BUILD OUR OWN ROUTER STATE 7 . 6
  • 54. The following is of interest: The url The router parameters The query parameters 7 . 7
  • 55. Define a serializer, that saves url, queryParams and router params interface MyState { url: string; queryParams; params; } export class MySerializer implements RouterStateSerializer<MyState> { serialize(routerState: RouterStateSnapshot): MyState { console.log('serializer'); console.log('complete router state', routerState); const { url, root: { queryParams, firstChild: { params } } return { url, queryParams, params }; } } 7 . 8
  • 56. Provide this as the 'new' serializer @NgModule({ providers: [{ provide: RouterStateSerializer, useClass: MySerializer }] }) 7 . 9
  • 57. This is what the router state looks like now, only saves exactly what we want 7 . 10
  • 59. Objective: We want to ensure we can carry out things like accessing resources over the network. 8 . 2
  • 60. INSTALLATION AND SETUPINSTALLATION AND SETUP npm install @ngrx/effects import { EffectsModule } from '@ngrx/effects'; @NgModule({ EffectsModule.forRoot([ ... my effects classes ]) }) 8 . 3
  • 61. What kind of behaviour do we want? Set a loading flag, show spinner Do AJAX call Show fetched data or error Set loading flag to false, hide spinner 8 . 4
  • 62. NGRX approach try { store.dispatch({ type: 'FETCHING_DATA' }) // state: { loading: true } const data = await getData(); // async operation store.dispatch({ type: 'FETCHED_DATA', payload: data }); // state: { loading: false, data: {/* data from endpoint */} } catch (error) { store.dispatch({ type: 'FETCHED_DATA_ERROR', payload: error }); } 8 . 5
  • 63. My first effect @Injectable() export class ProductEffects { @Effect() products$: Observable<Action> = this.actions$.pipe( ofType(FETCHING_PRODUCTS), switchMap( // ensure we dispatch an action the last thing we do action => of({ type: "SOME_ACTION" }) ) ); constructor(private actions$: Actions<Action>, private http: console.log("product effects init"); } } 8 . 6
  • 64. My first effect - calling HTTP @Injectable() export class ProductEffects { @Effect() products$: Observable<Action> = this.actions$.pipe( ofType(FETCHING_PRODUCTS), // listen to this action switchMap(action => this.http .get("data/products.json") .pipe( delay(3000), map(fetchProductsSuccessfully), // success catchError(err => of(fetchError(err))) // error ) ) ); 8 . 7
  • 65. ENTITESENTITES REDUCE THAT BORING BOILER PLATEREDUCE THAT BORING BOILER PLATE 9 . 1
  • 66. Install and set up npm install @ngrx/entity import { EntityState, createEntityAdapter, EntityAdapter } from "@ngrx/entity"; // set up the adapter const adapter: EntityAdapter<User> = createEntityAdapter<User>(); // set up initial state const initial = adapter.getInitialState({ ids: [], entities: {} }); 9 . 2
  • 67. Install and set up /* use the adapter methods for specific cases like adapter.addOne() */ function userReducer( state = initial, action: ActionPayload<User> ) { switch (action.type) { case "ADD_USER": return adapter.addOne(action.payload, state); default: return state; } } 9 . 3
  • 68. What else can it do for us? addOne: Add one entity to the collection addMany: Add multiple entities to the collection addAll: Replace current collection with provided collection removeOne: Remove one entity from the collection removeMany: Remove multiple entities from the collection removeAll: Clear entity collection updateOne: Update one entity in the collection updateMany: Update multiple entities in the collection 9 . 4
  • 70. SCHEMATICSSCHEMATICS BE LAZY AND SCAFFOLD :)BE LAZY AND SCAFFOLD :) 10 . 1
  • 71. It is a scaffolding library that helps us scaffold out NGRX features 10 . 2
  • 72. Schematics can help us scaffold the following: Action Container Effect Entity Feature Reducer Store 10 . 3
  • 73. Install and set up // install schematics and prerequisits npm install @ngrx/schematics --save-dev npm install @ngrx/{store,effects,entity,store-devtools} --save // we might need this one as well npm install --save @angular/cli@latest 10 . 4
  • 74. Scaffold, initial state set up, forRoot({}) and instrument() ng generate store State --root --module app.module.ts --collec // result @NgModule({ declarations: [ ... ], imports: [ BrowserModule, StoreModule.forRoot(reducers, { metaReducers }), !environment.production ? StoreDevtoolsModule.instrument() ], bootstrap: [AppComponent] }) export class AppModule { } 10 . 5
  • 75. Scaffold, setting up effects for the App ng generate effect App --root --module app.module.ts --collect @NgModule({ declarations: [ AppComponent ], imports: [ ... EffectsModule.forRoot([AppEffects]) ], providers: [], bootstrap: [ ... ] }) export class AppModule { } 10 . 6
  • 76. Scaffold, create Action, generates action and test ng generate action User --spec export enum UserActionTypes { UserAction = '[User] Action' } export class User implements Action { readonly type = UserActionTypes.UserAction; } export type UserActions = User; 10 . 7
  • 77. Scaffold, create a component, with store injected ng generate container TodoList @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', styleUrls: ['./todo-list.component.css'] }) export class TodoListComponent implements OnInit { constructor(private store: Store<any>) { } ngOnInit() { } } 10 . 8
  • 78. DO IT YOURSELFDO IT YOURSELF BUILD YOUR OWN NGRXBUILD YOUR OWN NGRX 11 . 1
  • 79. What requirements do we have? Should be possible to dispatch actions State should update when we dispatch It should be possible to subscribe to a slice of state We should support side effects 11 . 2
  • 80. BEHAVIOURSUBJECTBEHAVIOURSUBJECT SUPPORTS A STATE, CAN BESUPPORTS A STATE, CAN BE SUBSCRIBED TOSUBSCRIBED TO 11 . 3
  • 81. BehaviorSubject // ctor value = inital value let subject = new BehaviorSubject({ value: 1 }) subject.subscribe(data => console.log('data', data)); // { value: 1 } // { prop: 2} subject.next({ prop: 2 }); 11 . 4
  • 82. Merging states with .scan() let subject = new BehaviorSubject({ value: 1 }) // {} inital subject .scan((acc, value) =>({ ...acc, ...value })) .subscribe(data => console.log('data', data)); subject.next({ prop: 2 }); // { value: 1, prop: 2 } 11 . 5
  • 83. Implementing the store class Store extends BehaviorSubject { constructor(initialState = {}) { super(initialState); this.listenerMap = {}; this.dispatcher = new Subject(); this.dispatcher .scan((acc, value) =>({ ...acc, ...value })) .subscribe(state => super.next(state)); } dispatch(newState) { this.dispatcher.next(newState); } } 11 . 6
  • 84. Usage, store store = new Store(); store.subscribe(data => console.log('state', data)); store.dispatch({ val: 1 }); store.dispatch({ prop: 'string' }); // { val: 1, prop: 'string' } 11 . 7
  • 85. What about slice of state? class Store extends BehaviorSubject { constructor(initialState = {}) { ... } dispatch(newState) { this.dispatcher.next(newState); } select(slice) { return this.map[slice] } selectWithFn(fn) { return this.map(fn) } } 11 . 8
  • 86. We need to improve the core implementation, enter storeCalc() const storeCalc = (state, action) => { return { counter: countReducer(state.counter, action), products: productsReducer(state.products, action) } }; 11 . 9
  • 87. A retake on our dispatch(), old state, getValue() + action = new state dispatch(action) { const newState = storeCalc(this.getValue(),action); this.dispatcher.next(newState); } 11 . 10
  • 88. LETS' TALK ABOUT EFFECTSLETS' TALK ABOUT EFFECTS 11 . 11
  • 89. We need to be able to signup to specific actions We need to be able to carry out side effects 11 . 12
  • 90. First let's set up subscription in the store class Store { constructor() { ... } dispatch() { ... } select() { ... } effect(listenToAction, listener) { if(!this.listenerMap.hasOwnProperty(listenToAction)) { this.listenerMap[listenToAction] = []; } this.listenerMap[listenToAction].push( listener ); } } 11 . 13
  • 91. Then ensure the effect happens in dispatch() class Store { constructor() { ... } dispatch() { const newState = storeCalc(this.getValue(),action); this.dispatcher.next(newState); // tell our listeners this action.type happened if(this.listenerMap[action.type]) { this.listenerMap[action.type].forEach(listener => { listener(this.dispatch.bind(this),action); }); } } select() { ... } effect(listenToAction, listener) { ... } } 11 . 14
  • 92. use our new effect() method let store = new Store(); store.effect('INCREMENT' ,async(dispatch, action) => { // side effect let products = await getProducts(); // side effect let data = await getData(); // dispatch, if we want dispatch({ type: 'INCREMENT' }); }) store.dispatch({ type: 'DECREMENT' }); 11 . 15
  • 94. We learned how to: Grasp the basics of Redux NGRX building blocks Use the store Leverage the dev tools and its Redux plugin Store our router state and transform it How we handle side effect like AJAX calls Remove boiler plate with Entity How to be even lazier with the scaffold tool Schematics Upgrading ourselves to Ninja level by learning how to implement NGRX 12 . 2
  • 95. Further reading: Free video course, https://platform.ultimateangular.com/courses/ngrx-st effects Redux docs, https://redux.js.org/docs Probably the best homepage on it, Brian Troncone, https://gist.github.com/btroncone/a6e4347326749f93 12 . 3
  • 96. Buy my book ( please :) ): https://www.packtpub.com/web- development/architecting-angular-applications-flux- redux-ngrx 12 . 4
  • 97. Thank you for listening 12 . 5