With NGRX 8 and createAction, how can I use strongly-typed #Effect? - ngrx

I'm migrating a large codebase from NGRX 7 to 8.
My actions look like this:
export class DeleteRecordAction implements Action {
readonly type = ActionTypes.DELETE_RECORD;
constructor(public payload: IRecord);
}
export class DeleteRecordSuccessAction implements Action {
readonly type = ActionTypes.DELETE_RECORD_SUCCESS;
}
export class DeleteRecordFailureAction implements Action {
readonly type = ActionTypes.DELETE_RECORD_FAILURE;
}
and my effects look like this (note the strongly-typed return Observable):
#Effect() deleteRecord$: Observable<DeleteRecordSuccessAction | DeleteRecordFailureAction> =
this.actions$.pipe(
ofType<Actions.DeleteRecordAction>(ActionTypes.DELETE_RECORD),
switchMap((action) => this.recordService.deleteRecord(action.payload).pipe(
map((result) => new Actions.DeleteRecordSuccessAction()),
catchError((err) => of(new Actions.DeleteRecordFailureAction())),
)),
);
I'm trying to migrate the actions to use the new createAction syntax, which is easy enough:
export const DeleteRecordAction = createAction('[FOO] Delete Record', props<{ payload: IRecord }>());
export const DeleteRecordSuccessAction = createAction('[FOO] Delete Record Success');
export const DeleteRecordFailureAction = createAction('[FOO] Delete Record Failure');
but now my #Effect won't compile at the following line:
#Effect() deleteRecord$: Observable<DeleteRecordSuccessAction | DeleteRecordFailureAction> =
with the errors:
TS2749: 'DeleteRecordSuccessAction' refers to a value, but is being used as a type here.
TS2749: 'DeleteRecordFailureAction' refers to a value, but is being used as a type here.
I can remove the strong-typing from the #Effect return value and just use the generic Action, like this:
#Effect() deleteRecord$: Observable<Action> =
but then I lose the useful type-checking of the return values, thereby making the code more error-prone.
The new createEffect method doesn't seem to help. Is there a way to use NGRX 8 while retaining strong-typing on effects? Or should I just stick with NGRX 7 syntax?

Related

Trouble reading documentation Interface Definition

I'm trying to figure out how a Redux createStore function works(what parameters it accepts) with enhancers from the documentation.
what I understand is, "sayHiOnDispatch" takes a "createStore" function as a parameter and creates a closure around the inner anonymous function which accepts 3 arguments,
rootReducer, preloadedState, enhancers
finally, it return an object {...store,dispatch:newDispatch}.
what I don't understand is: 1)Where is sayHiOnDispatch is being called from?
2)How the anonymous function is getting called?
3)What variable receives the return value of return { ...store, dispatch: newDispatch }
4)What calls the newDispatch functions?
5)How can I understand the function structure(params, return values, etc..) from the Interface Definition?
export type StoreEnhancer<Ext = {}, StateExt = never> = (
next: StoreEnhancerStoreCreator<Ext, StateExt>
) => StoreEnhancerStoreCreator<Ext, StateExt>
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
S = any,
A extends Action = AnyAction
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
The redux tutorial code.
export const sayHiOnDispatch = (createStore) => {
return (rootReducer, preloadedState, enhancers) => {
const store = createStore(rootReducer, preloadedState, enhancers)
function newDispatch(action) {
const result = store.dispatch(action)
console.log('Hi!')
return result
}
return { ...store, dispatch: newDispatch }
}
}
Hopefully, someone will provide a fishing-rod for me to fish the fish.
Answering the questions in order:
sayHiOnDispatch gets called either as return enhancer(createStore)(reducer, preloadedState) inside of createStore itself, or on the same line as part of a "composed" enhancer (like compose(applyMiddleware(), sayHiOnDispatch) )
The anonymous function is called on that same line - it's the return value from enhancer()
The returned {...store, dispatch: newDispatch} is the actual store variable, as in const store = createStore()
newDispatch is the actual store.dispatch function, so it's called as store.dispatch(someAction)
Yeah, that is definitely a complex type signature :) To be honest I wouldn't worry about it - odds are you won't ever end up writing an enhancer yourself.

Rewrite redux-orm reducer with redux-toolkit

Issue (tl;dr)
How can we create a custom redux-orm reducer with redux-toolkit's createSlice?
Is there a simpler, recommended, more elegant or just other solution than the attempt provided in this question?
Details
The example of a custom redux-orm reducer looks as follows (simplified):
function ormReducer(dbState, action) {
const session = orm.session(dbState);
const { Book } = session;
switch (action.type) {
case 'CREATE_BOOK':
Book.create(action.payload);
break;
case 'REMOVE_AUTHOR_FROM_BOOK':
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
break;
case 'ASSIGN_PUBLISHER':
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
break;
}
return session.state;
}
It's possible to simplify reducers with the createSlice function of redux-toolkit (based on the redux-toolkit usage-guide):
const ormSlice = createSlice({
name: 'orm',
initialState: [],
reducers: {
createBook(state, action) {},
removeAuthorFromBook(state, action) {},
assignPublisher(state, action) {}
}
})
const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer
However, at the beginning of redux-orm reducer we need to create a session
const session = orm.session(dbState);
then we do our redux-orm reducer magic, and at the end we need to return the state
return session.state;
So we miss something like beforeEachReducer and afterEachReducer methods in the createSlice to add this functionality.
Solution (attempt)
We created a withSession higher-order function that creates the session and returns the new state.
const withSession = reducer => (state, action) => {
const session = orm.session(state);
reducer(session, action);
return session.state;
}
We need to wrap every reducer logic in this withSession.
import { createSlice } from '#reduxjs/toolkit';
import orm from './models/orm'; // defined elsewhere
// also define or import withSession here
const ormSlice = createSlice({
name: 'orm',
initialState: orm.session().state, // we need to provide the initial state
reducers: {
createBook: withSession((session, action) => {
session.Book.create(action.payload);
}),
removeAuthorFromBook: withSession((session, action) => {
session.Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
}),
assignPublisher: withSession((session, action) => {
session.Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
}),
}
})
const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer
This is a fascinating question for me, because I created Redux Toolkit, and I wrote extensively about using Redux-ORM in my "Practical Redux" tutorial series.
Off the top of my head, I'd have to say your withSession() wrapper looks like the best approach for now.
At the same time, I'm not sure that using Redux-ORM and createSlice() together really gets you a lot of benefit. You're not making use of Immer's immutable update capabilities inside, since Redux-ORM is handling updates within the models. The only real benefit in this case is generating the action creators and action types.
You might be better off just calling createAction() separately, and using the original reducer form with the generated action types in the switch statement:
export const createBook = createAction("books/create");
export const removeAuthorFromBook = createAction("books/removeAuthor");
export const assignPublisher = createAction("books/assignPublisher");
export default function ormReducer(dbState, action) {
const session = orm.session(dbState);
const { Book } = session;
switch (action.type) {
case createBook.type:
Book.create(action.payload);
break;
case removeAuthorFromBook.type:
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
break;
case assignPublisher.type:
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
break;
}
return session.state;
}
I see what you're saying about adding some kind of "before/after" handlers, but that would add too much complexity. RTK is intended to handle the 80% use case, and the TS types for createSlice are already incredibly complicated. Adding any more complexity here would be bad.
I came across this question looking to combine the benefits of redux-toolkit
and redux-orm. I was able to come up with a solution I've been pretty happy
with so far. Here is what my redux-orm model looks like:
class Book extends Model {
static modelName = 'Book';
// Declare your related fields.
static fields = {
id: attr(), // non-relational field for any value; optional but highly recommended
name: attr(),
// foreign key field
publisherId: fk({
to: 'Publisher',
as: 'publisher',
relatedName: 'books',
}),
authors: many('Author', 'books'),
};
static slice = createSlice({
name: 'BookSlice',
// The "state" (Book) is coming from the redux-orm reducer, and so will
// never be undefined; therefore, `initialState` is not needed.
initialState: undefined,
reducers: {
createBook(Book, action) {
Book.create(action.payload);
},
removeAuthorFromBook(Book, action) {
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
},
assignPublisher(Book, action) {
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
}
}
});
toString() {
return `Book: ${this.name}`;
}
// Declare any static or instance methods you need.
}
export default Book;
export const { createBook, removeAuthorFromBook, assignPublisher } = Book.slice.actions;
The redux-toolkit slice is created as a static property on the class, and then
the model and its actions are exported in a manner similar to Ducks
(ORMDucks??).
The only other modification to make is to define a custom updater for
redux-orm's reducer:
const ormReducer = createReducer(orm, function (session, action) {
session.sessionBoundModels.forEach(modelClass => {
if (typeof modelClass.slice.reducer === 'function') {
modelClass.slice.reducer(modelClass, action, session);
}
});
});
See a more complete example here:
https://gist.github.com/JoshuaCWebDeveloper/25a302ec891acb6c4992fe137736160f
Some Notes
#markerikson makes a good point about some of the features of redux-toolkit
not being used since redux-orm is managing the state. For me, the two
greatest benefits of using this method are not having to wrangle a whole
bunch of action creators and not having to contend with awful switch
statements :D.
I am using the stage 3 class fields and static class features proposals. (See
https://babeljs.io/docs/en/babel-plugin-proposal-class-properties). To make
this ES6 compatible, you can easily refactor the model class to define its
static props using the current syntax (i.e. Book.modelName = 'Book';).
If you decide to mix models like the one above with models that don't define
a slice, then you'll need to tweak the logic in the createReducer updater
slightly.
For a real world example, see how I use the model in my project here:
https://github.com/vallerance/react-orcus/blob/70a389000b6cb4a00793b723a25cac52f6da519b/src/redux/models/OrcusApp.js.
This project is still in the early stages. The largest question in my mind is
how well this method will scale; however, I am optimistic that it will continue
to provide numerous benefits as my project matures.
Try using normalized-reducer. It's a higher-order-reducer that takes a schema describing the relationships, and returns a reducer, action, and selectors that write/read according to the relationships.
It also integrates easily with Normalizr and Redux Toolkit.

Conditonally returning Observable<Action> within Ngrx Effect

I'm currently refactoring my code to include the ngrx store. To minimise the amount of API calls of my LoadDeals() action, I'm checking in an effect if the store is empty. Only if it is empty, go on and make an API call. I first tried to use this pattern found here on SO (https://stackoverflow.com/a/50527652/2879771).
I realised the downside is, that every LoadDeals() call is ignored if there is data in the store. To have the possibility of forcing a load, I included an optional boolean payload to the LoadDeals() class. If this is set to true, do call the API.
Here's my first try
#Effect() loadDeals$: Observable<Action> = this._actions$.pipe(
ofType<LoadDeals>(actions.LOAD_DEALS),
withLatestFrom(this._store.pipe(select(getDealsLoaded))),
switchMap(([action, hasLoaded]) => {
if (!hasLoaded || action.force) {
return this._deals.deals.pipe(
map(deals => new LoadDealsSuccess(deals)),
catchError(error => of(new LoadDealsFail(error)))
);
} else {
return of(new LoadDealsSkip());
}
})
);
But this yields the following error:
Argument of type '([action, hasLoaded]: [LoadDeals, boolean]) => Observable<LoadDealsSuccess | LoadDealsFail> | Observable<LoadDealsSkip>' is not assignable to parameter of type '(value: [LoadDeals, boolean], index: number) => ObservableInput<LoadDealsSuccess | LoadDealsFail>'.
Type 'Observable<LoadDealsSuccess | LoadDealsFail> | Observable<LoadDealsSkip>' is not assignable to type 'ObservableInput<LoadDealsSuccess | LoadDealsFail>'.
Type 'Observable<LoadDealsSkip>' is not assignable to type 'ObservableInput<LoadDealsSuccess | LoadDealsFail>'.
Type 'Observable<LoadDealsSkip>' is not assignable to type 'Iterable<LoadDealsSuccess | LoadDealsFail>'.
Property '[Symbol.iterator]' is missing in type 'Observable<LoadDealsSkip>'.
Here is my deals.actions.ts
import { Action } from '#ngrx/store';
import { ITmsCpRecord } from '#models/cp';
export enum DealsActionTypes {
LOAD_DEALS = '[Deals] Load Deals',
LOAD_DEALS_FAIL = '[Deals API] Load Deals Fail',
LOAD_DEALS_SUCCESS = '[Deals API] Load Deals Success',
LOAD_DEALS_SKIP = '[Deals Store] Load Deals Skip (cached)'
}
export class LoadDeals implements Action {
readonly type = DealsActionTypes.LOAD_DEALS;
constructor(public force: boolean = false) {}
}
export class LoadDealsFail implements Action {
readonly type = DealsActionTypes.LOAD_DEALS_FAIL;
constructor(public payload: any) {}
}
export class LoadDealsSuccess implements Action {
readonly type = DealsActionTypes.LOAD_DEALS_SUCCESS;
constructor(public payload: ITmsCpRecord[]) {}
}
export class LoadDealsSkip implements Action {
readonly type = DealsActionTypes.LOAD_DEALS_SKIP;
}
// action types
export type DealsAction = LoadDeals|LoadDealsFail|LoadDealsSuccess|LoadDealsSkip;
So I split it into separate effects listening to the same action but with a different filter operator. This works fine. Although I would like to not split it because it is some redundant code.
Does someone see my error? Cheers
#Effect() loadDeals$: Observable<Action> = this._actions$.pipe(
ofType<LoadDeals>(actions.LOAD_DEALS),
withLatestFrom(this._store.pipe(select(getDealsLoaded))),
filter(([action, hasLoaded]) => !hasLoaded || action.force),
switchMap(() => {
return this._deals.deals.pipe(
map(deals => new LoadDealsSuccess(deals)),
catchError(error => of(new LoadDealsFail(error)))
);
})
);
#Effect() loadDealsSkip$: Observable<Action> = this._actions$.pipe(
ofType<LoadDeals>(actions.LOAD_DEALS),
withLatestFrom(this._store.pipe(select(getDealsLoaded))),
filter(([action, hasLoaded]) => hasLoaded && !action.force),
switchMap(() => of(new LoadDealsSkip()))
);
You will probably not like this answer, but I would suggest to create 2 actions for this.
One load action like you already have, and another force load action.
This would also mean creating 2 effects, but in the second one you won't have to select from store.

Container Component not updating on action

I have the following reducer:
const selectEntityReducer = function(entityState, action) {
const selectedEntity = entityState.allEntities.filter(e => (e.id == action.id))[0]
const newStateForSelectEntity = updateObject(entityState, {selectedEntity: selectedEntity});
return newStateForSelectEntity;
};
entityState shape looks like {entities:[{id:1}, {id:2}], selectedEntity:{id:2}}.
I have a React component that is connected to the store like this:
function mapStateToProps(state) {
return {
selectEntity: state.entities.selectEntity
};
}
However, it never re-renders.
If I say selectEntities: state.entities in mapStateToProps, then the component re-renders. But I'm only interested in the selected entity for this component.
I'm aware that enforcing immutability down the entire state tree is required for redux to function properly. But I thought that is what I was doing in selectEntityReducer.
Any extra context can be seen in the full redux code in this gist.
Its happens because you filter old piece of state.
You must copy your entities:
const newEntities = entityState.allEntities.slice();
const newSelectedentities = newEntities.filter.........
Copy piece of ctate if it array and then process it.

How to subscribe to service that receives data from router (master-detail scenario)?

I am new to Angular2 and typescript, so this may be an easy one for any experienced folks! I develop a master-detail App using the Angular Heroes tutorial as a starter. I added a http service that calls a rest api. It properly lists decisions made in our town ( Antwerp Belgium :) ). Now when I click to view the details, the resulting json object is 'undefined'.
In my decision-detail.component.ts file, I have an error at decision => this.decision = decision, "Type 'Decision[]' is not assignable to type 'Decision'."
export class DecisionDetailComponent implements OnInit {
#Input() decision: Decision;
errorMessage: string;
constructor(
private _decisionService: DecisionService,
private _routeParams: RouteParams) {
}
ngOnInit() {
let id = +this._routeParams.get('id');
this._decisionService.getDecision(id)
.subscribe(
decision => this.decision = decision,
error => this.errorMessage = <any>error);
}
}
Here's my decision.service.ts code:
getDecision (id: number) {
return this.http.get(this._decisionsUrl+id)
.map(res => <Decision[]> res.json().data)
.do(data => console.log(data)) //eyeball results in the console
.catch(this.handleError)
Any help much appreciated!!
I got this working. Deleted the #input and declared decisions as a property in the class.

Resources