-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(store): add createReducer function #1746
Conversation
Preview docs changes for 3f2befd at https://previews.ngrx.io/pr1746-3f2befd/ |
The creation of reducers is very nice, and as you mentioned, having to add more explicit type checking on each function is unfortunate. Both that and having to wrap the reducers for each feature feels like a step back in ease of use upfront even if you gain a less verbose way of creating a reducer function. |
Preview docs changes for dd3d865 at https://previews.ngrx.io/pr1746-dd3d865/ |
@alex-okrushko is this initial set of work ready to merge? |
This one uses |
We can land with the token now, and follow-up with the fn if you prefer. |
Preview docs changes for 5704f62 at https://previews.ngrx.io/pr1746-5704f62/ |
FYI,
|
Preview docs changes for 0bb7409 at https://previews.ngrx.io/pr1746-0bb7409/ |
projects/example-app/src/app/books/reducers/collection.reducer.ts
Outdated
Show resolved
Hide resolved
My vote goes to using |
How would we feel about having the initial state as first argument? It would allow to skip the array, but perhaps it's useful to have the distinction between the two? export const reducer = createReducer<State>(
initialState,
on(AuthApiActions.loginSuccess, (state, { user }) => ({ ...state, user })),
on(AuthActions.logout, () => initialState)
); Implementation: export function createReducer<S>(
initialState: S,
...ons: { reducer: On<S>; types: string[] }[]
): Reducer<S> {
const map = new Map<string, On<S>>();
for (let on of ons) {
for (let type of on.types) {
map.set(type, on.reducer);
}
}
return function(state: S = initialState, action: Action): S {
const reducer = map.get(action.type);
return reducer ? reducer(state, action) : state;
};
} |
I thought about that as well, but the |
As an additional note, you won't be able to use export function reducer(state, action) {
return createReducer(...);
}
StoreModule.forFeature('feat', reducer) |
We want to treat Actions like Events... In DOM, events listeners use 'on' prefix, so that's another ➕for
Will look into that as well. |
@alex-okrushko sounds like |
LGTM other than Tim's comments |
Preview docs changes for c3816d4 at https://previews.ngrx.io/pr1746-c3816d4/ |
Preview docs changes for be5f4d8 at https://previews.ngrx.io/pr1746-be5f4d8/ |
Should be ready to go 😀 |
@timdeschryver @brandonroberts
I was trying to remove the need to provide the generic for the I'd like to change it to the following now: export const reducer = createReducer(
initialState,
on(AuthApiActions.loginSuccess, (state, { user }) => ({ ...state, user })),
on(AuthActions.logout, () => initialState)
); |
I believe it would be beneficial if this was also included in the docs as an AoT comment. |
Will you leave a comment about this in #1762? |
Will do :) |
Hi @alex-okrushko ! Would mind giving a little background on why this change was made? When would you suggest using the injection token approach over the standard one? Thanks! |
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
Introduces
createReducer
function, that has the same functionality asreducer
fromts-actions
(created by @cartant).It helps remove Action unions and dramatically reduce the code needed for reducers. Compare:
However, there are a few caveats that are caused by microsoft/TypeScript#3755 and microsoft/TypeScript#7547 (comment) with the latter acknowledging current TS limitation.
Sometimes
reducer
functions inon(...)
have to be explicitly typed with their return type. Let's look at the example:If the
State
inon(LayoutActions.closeSidenav, (): State
is not provided it would narrow the type to{showSidenav: false}
and{showSidenav: true}
correspondingly, which would not match the interface ofState
(which is{showSidenav: boolean}
).TS highlights this as error, so providing the
State
is requiredSometimes typos could be missed
Let's look at another example (reduced for brevity):
Note the
ending: false
, that missedp
. The original intention was to spread the state and override withpending: false
, however because of the typopending
won't be overridden and instead extraending
property will be added.This will not match the
State
interface, butState
could be extended from this interface with type (because entirestate
of typeState
is spread).switch-based reducer function catches these types of errors, because providing the return type of the
reducer
function is a norm:function reducer(state: State = initialState, action: Action): State
.Solution: This unfortunate error can be also caught by explicitly typing the
reducer
withinon
:on(AuthApiActions.loginFailure, (state, { error }): State => ({..})),
For me, that's an acceptable solution. Maybe we should recommend to always type it. I wish the
createSelector<State>
would've caught it, but it's not currently possible due to TS limitations mentioned above.Note: if the
State
typed could not be extracted from the returned object literal, the error would be displayed property even without explicit return type, e.g.:Closes # #1724
Does this PR introduce a breaking change?
Other information