- Actions
- Reducer : action -> new state
- Selectors: get state info
- Effects : SIDE - Effects von actions , die nicht den state beeinflussen
- EntityState: handles collections of entities that are saved in the store as state.
ng add @ngrx/store
ng add @ngrx/store-devtools
ng generate store auth/Auth --module auth.module.ts
wennauth
der pfad zum auth.module.ts ist. (evtl. muss hier noch korrigiert werden:import { environment } from '../../../environments/environment';
)StoreModule.forFeature('auth', fromAuth.reducers)
hier istauth
der name für den Bereich , zu sehen z.B. im NgRx Store DevTools -> State -> Raw
- store injekten: im Constructor füge hinzu:
private store : Store<State>
, wobeiimport { Store } from "@ngrx/store";
, undState
der Typ ist der imapp.module.ts
in ..imports...
beiStoreModule.forRoot(reducers, { metaReducers }),
als erster parameter vonforRoot()
definiert ist.- hier also
reducers
, welches seinerseits eininterface
ist das von app.modules.ts aus gesehen inapp/reducers/index.ts
exportiert wurde:export interface State {
(NICHT das , das inapp/auth/reducers/index.ts
definiert wurde! ) - am besten in
app/reducers/index.ts
:State
->AppState
umbenennen weil das ist der "global" - State !
- der
Store<State>
ist auch einObservable
- der injectete
store
kann nicht direkt geändert werden, sondern Änderungen werden über dessendispatch(Action)
publiziert.
- plain JavaScript Object für
dispatch()
mit einemtype
und (meistens) einempayload
child.- can be a command, or
- öfter noch ein event , der eine Änderung auf level der Componente mitteilt.
- der Store selbst entscheidet, was mit einer Action konkret zu tun ist. defaultmäßig passiert erstmal gar nicht damit, auch der State wird nicht geändert.
- Vorteil: lose coupling
- Empfehlung: statt plain JavaScript Object besser ein explizit factory methode definieren und dann typesave verwenden. Die factory methode verwendet ihrerseits
createAction()
aus@ngrx/store
:
export const login = createAction(
"[Login Page] " //source, i.e.: "type" of the action
,props<{user:User}>()// props() from "@ngrx/store"
)
//and in the compoentent itself , which will deliver the user profile that needs to be dispached to the store:
...
someObservable.pipe().(tap(user => dispatch(login({user:user})))).subscribe(user => ...)
given myAction = createAction(..)
then you can filter in pipe(): ofType(myAction)
for exactly this action
for some reason, Actions are imported and re-exportet in app/auth/action-types.ts
. Seems to be merily a typescript - trick to get command-line - completion .
- defines a function, what the store should do in response to Actions the are dispached to it.
- this function can be created from a factory-function called
createReducer()
from@ngrx/store
, possibly using theon()
function :
export const authReducer = createReducer(
initialState,
on(AuthAction.login,(state,action) => {
return {
user: action.user
}
}),
//more reducers based on actions can be defined here
);
- this
authReducer
then must be added to the *.module.ts - file for this module, in the@NgModules
->imports:
->StoreModule.forFeature('auth', authReducer)
- a reducer should not return a modified previous state, it should instead return a new constructed/copied state
- are executed BEFORE the normal reducers.
- are executed in exactly the order given in
reducers/index.ts
in the Array :export const metaReducers: MetaReducer<AppState>[] = !environment.production ? [] : [];
- a metaReducer must
- implement:
function logger(reducer: ActionReducer<any>):ActionReducer<any>
- this function must return another 2nd anonymous function which in turn takes (state,action)
- this 2nd function must call the reducer on the
return
statement using the parameters (state,action):return reducer(state,action)
- implement:
- Developers can think of meta-reducers as hooks into the action->reducer pipeline. Meta-reducers allow developers to pre-process actions before normal reducers are invoked.
- e.g.: Using a meta-reducer to log all actions
- inject the
statestore (Typeprivate store : Store<AppState>
) in the component via constructor, the type parameter either the root of the state or some child state. the root State is usually exportet in./reducers/index.ts
as a plain interface - use the
*ngIf="myobs$ | async"
to decide , whether the UI Element should be there - take the state (which is already an observable ),
state.pipe(map(state => ...))
and map it to a boolean value and set this to yourmyobs$
see angular_rxjs#ngRx Custom Operators for Observables
- to avoid recalculating a mapping from a state/Store, if the relevant part of the state has not change
- use
createSelector()
from@ngrx/store
, define this in own seperate source file calledxx.selector.ts
createSelector(selector , projector )
arguments are explained here- the
selector
is memoized for optimal performance, createSelector(selector , projector ) creates a mapping function, that has a memory and only makes the calculation if the arguments are not yet previousely seen. Otherwise it just returns the result from its cache. - use this in combination :
observable$.pipe(select(myselector))
if e.g. the inverse output from selectorFunctionA is needed you can write:
export const selectorFunctionB = createSelector(
selectorFunctionA,
resultA => !resultA
)
- to select a specific part of the global state/store in a typesafe way:
export const authSelector = createFeatureSelector<AuthState>("auth")
if "AuthState" is the type of the part of the global state/store we want to have selected and "auth" is the name of it. eg.:
{
auth: {
user: {
id: "1"
email: "[email protected]"
}
}
}
- result can be used as the first argument to
createSelector(..,..)
this.store
.select<any>((state: any) => state) // the complete state this time!!!
.subscribe((completeState: any) => console.log(completeState));
- an Effect is a Service that gets all Actions, just like Reducers, and makes a side - effect from some of the Actions (so Actions are normaly filtered first).
- add the
EffectsModule.forRoot([])
to app.module.ts - > imports: - add the
EffectsModule.forFeature([])
to a submodule-imports - a Effect is a Service class, so you need
@Injectable()
- the important part here is the
constructor(private actions$:Actions)
- here you filter for the relevant actions see Action Filter above
- you could
subscribe()
and perform your code in the subscribe, but better usetap()
in thepipe()
after theofType()
-filter, BECAUSE then you get type-information for the action (from theofType()
-filter before thetap()
)
- using
createEffect()
from@ngrx/effects
instead of constructor is even better, e.g. because of error handling. - instead for deriving a new observable from the action$ and subscribe to it,
createEffect()
takes the observable and subscribe to it automaticaly - an effect that makes an side-effect from actions but dispaches no other actions
@Injectable()
export class AuthEffects {
login$ = createEffect(()=>this.actions$.pipe(
ofType(AuthAction.login),
tap(action => localStorage.setItem('myKey', JSON.stringify(action.user)))
),
{dispatch:false})// don't forget this
constructor(private actions$: Actions) {}
}
maps one action to another action , while possibly doing some side effect
effectName$ = createEffect(
() => this.actions$.pipe(
ofType(FeatureActions.actionOne),
map((myObject) => FeatureActions.actionTwo(myObject))
)
);
so in this case the last map operation needs to return (not dispatch()
) the new action
- Reducer gets an Action and changes the State
- Effects gets an Action and does a Sideeffect and dispaches another action or no action
- Effect-Class and Reducer-Class, if you have both Effect-class and Reducer-class react to the same action type, Reducer-class will react first, and then Effect-class
- definition of a Reducer always needs an initial state defined.
- Effects don't do anything with the State/Store
- zusammen mit @ngrx/store-devtools (siehe oben)
- debugging the router states: add to
app.module.ts
->@NgModules
->imports
StoreRouterConnectionModule.forRoot({ stateKey:'router',routerState:RouterState.Minimal})
- and add to
app/reducers/index.ts
:export const reducers: ActionReducerMap<AppState> = {router:routerReducer};
- and add to
- runtime check for NOT mutating of the store - state: add in
app.module.ts
->@NgModules
->imports
:
StoreModule.forRoot(reducers, { metaReducers,
runtimeChecks:{
strictStateImmutability: true,
strictActionImmutability: true,
strictActionSerializability: true,
strictStateSerializability: true}
})
entities: {[key:number]:MyClass}
- for easy search using the key
- additional indices possible using
myIndex:number[]
- define a MyClassStore interface that extends
EntitieState<MyClass>
in myclass.reducers.ts - optional add additional fields, eg.: a "loaded"-Flag if needed
- use
EntityAdapter<MyClass>
byadapter = createEntityAdapter<MyClass>()
export
the adapter as aconst
(no Class required)- use adapters methods, e.g. :
adapter.getInitialState()
andadapter.addAll()
to get or add data. - add the new reducer to xxx.module.ts to
imports
:StoreModule.forFeature('myClassName', myReducer)
- Docu
{sortComparer:mySortFunction,...}
comparer for sorting (only the primary index table "ids", not the "entities" gets sortet){selectId:myId,...}
replacement for the "id:" - default property , if we want to use "courseId:" instead for just "id:"
use Update<T>
from '@ngrx/entity' as an argument to the action that changes the store.
- own file courses.selector.ts
- create a
selectMyClassState
for the "MyClass" feature bycreateFeatureSelector<EntityState<MyClass>>
as a BASE for alle other selectors - create the actual selectors (using
selectMyClassState
) by usingcreateSelector()
- either use the
EntityAdapter<MyClass>
-select-methods-(pointer!) , we can get fromadapter.getSelectors()
increateSelector()
- OR : first arg of
createSelector()
is another Selector for Pre-Selection, 2nd arg is the actual selector, which is a lambda or another method-pointer (which can be derived fromEntityAdapter<MyClass>.getSelectors())
.
- either use the
- selectors can be stacked on each other : a new selector uses existing selectors as pre-selectors
- inject the
Store<MyState>
into a class constructor (the Store is already a Injectable instantiatet automaticaly by StoreModule) obs$ = this.store.pipe(select(ngrx-selector-defined-previousely))
wheraspipe(select())
is equal topipe(map(),distinctUnitilChanged())
- add
EntityDataModule.forRoot({})
toapp.module.ts
->imports:
Config{}
empty, if no entities are directly associated with app.module but with a child-module - im child-module: define "const" - configurationVariable of Type
EntityMetadataMap
, with at least one element "MyEntity" (singular) - inject the
EntityDefinitionService
to constructor of the ChildModule class andregister()
thisEntityMetadataMap
- object defined before as a config to theEntityDefinitionService
- all entities need to be configured in the
EntityMetadataMap
- create an MyClassEntityService that extends
EntityCollectionServiceBase
as a *.service.ts file and make it@Injectable()
- its constructor needs a
EntityCollectionServiceElementsFactory
and pass this tosuper()
as 2nd arg, together with the name of the entityMyClass
as a string as its 1st arg. - this service needs to be defined as a
provider
and injected inChildModule
sconstructor()
- the loading from backend and storing in the local store still needs to be plugged in somewhere. Good place is the route - resolver, just like with see Router Resolver
- inject the
MyClassEntityService
in to the MyResolverService which should implementResolve<T>
- use the "loaded" and the "loading" - flags via
MyClassEntityService.loaded$
Observable to decide whether the data are already in the store. only if they are not , get the data from the backend viaMyClassEntityService.getAll()
- the overwritten
resolve()
- method should return theMyClassEntityService.loaded$
. In additon , we pipe the Observable andtap(loaded => ...)
it. In thetap()
, we get the data from the backend, only if loaded was false - after the
tap()
, we need to filter forloaded==true
and then we only interested in thefirst()
element - first() is mandatory, since the Router otherwise would wait indefinitely , until the Observable is completed
- to adapt rgrx Data to non-standard-convention data-structure from the backend
- for this ngrx Data uses another Service:
myclass-data.service.ts
as@Injectable()
- create a new MyDataSerivce class that extends the
DefaultDataService<MyClass>
- inject the
HttpClient
via itsconstructor()
- inject also a HttpUrlGenerator
- pass them to the
super()
constuctor in addition to to the 'MyClass' -String - plug MyDataSerivce in to the xxx.module.ts as a provider AND inject it in the ModuleClass constructor AND inject also the generic
EntityDataService
(import from ngrx data) AND inject the MyDataSerivce AND register MyDataSerivce toEntityDataService
, 1st arg: 'MyEntityName' (eg. 'Courses') end arg instance of MyDataSerivce - the actual customization is done in the MyDataSerivce by overwriting some methods from its base class.
- overwriting the
getAll()
method , we can change the URL and the data-mapping from the default behaviour.
- add a
sortComparar
toEntityMetadataMap
- works via
MyClassEntityService extends EntityCollectionServiceBase<MyClass>
- default behaviour for
update()
is "pessimisic" , can be changes inEntityMetadataMap
config - new Entities via
add()
; add should be kept pessimistic because IDs might be generated on the backend only. -> therefore thedialogue.close()
should be performed after the data are saved delete()
: is optimistic by default
- use
EntityCollectionServiceBase<MyClass>.entity$.getWithQueriy()
getWithQuery()
args are a json depending on the backend:
getWithQuery({
'myarg1':whatever.toString(),
'myarg2':whatever2.toString()
})
@Component({
selector: 'home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush // should work out of the box
// with ngrx data , as long as in the html template only async pipes are used for the data
})
export class HomeComponent
- All reducers that potentially change the state or the (feature-) slice of the state need to be together in one module/feature
- but you can read not only the state of the local feature-slice but always also the whole global state. You just can not WRITE from an external/another module/feature. BUT lazy loaded modules could not be loaded yet, so the corresponding state/slice could not exist yet.
- you can dispatch action in another module, can you really ? be aware of violating dependeny cycles to other modules!
to load data 2 possibilities:
- trigger a "Load" - Action on ngOnInit of the Component to be displayed now, and let an effect then load the data, then the effect triggers another action to store the new data in the store. -> needs 2 actions: one for tigger loading from backend, one for trigger loading data into the store
- or: make a Route Guard for the Components route and load the data in the
canActivate
function and then trigger an event to push these data into the store -> needs only 1 action plus one route guard.
atvantage of route guard: if there is an error , the browser does not show the Component matching the route but stays on the current route because the route guard is false. so if we want to show the error message to the user , we need to show in on the current (old) route instead in the Component matching the target route.
see : preloading-ngrx-store-route-guards
- After a asynchronos "save" data effect you can either just fire a "save success" event that set a "success flag" in the state, which you can use to show a "success" Snackbar.
- you can send a "redirect" action , which in turn should trigger a Router - Event , that inturn redirects the user back to a overview page and only then optionally in addition show a success if necessary.
- same for errors: a asynchronos "save" data effect you can either just fire a "save erro" event that set a "error flag" in the state, which you can use to show a "error" Snackbar.
- you can send a "redirect" action , which in turn should trigger a Router - Event , that inturn redirects the user back to a error page.
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BookListComponent],
schemas: [NO_ERRORS_SCHEMA],
providers: [
provideMockStore<AppState>({
initialState: mockStateWithBooksEntities()
})
]
}).compileComponents();
fixture = TestBed.createComponent(BookListComponent);
component = fixture.componentInstance;
store = TestBed.inject(MockStore);
dispatchSpy = spyOn(store, 'dispatch');
});
beforeEach(() => {
@NgModule({
imports: [
StoreModule.forFeature('cars', carsReducer, { initialState }),
EffectsModule.forFeature([CarsEffects])
],
providers: [CarsComponent]
})
class CustomFeatureModule {}
@NgModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
CustomFeatureModule,
]
})
class RootModule {}
TestBed.configureTestingModule({ imports: [RootModule] });
store = TestBed.get(Store);
from : https://thomasburlesonia.medium.com/ngrx-facades-better-state-management-82a04b9a1e39