-
-
Notifications
You must be signed in to change notification settings - Fork 1.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
Add ability for slices to listen to other actions #83
Conversation
Deploy preview for redux-starter-kit-docs ready! Built with commit 4c85493 https://deploy-preview-83--redux-starter-kit-docs.netlify.com |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks legit to me from a TypeScript perspective, I left just two small comments (see below).
However, on a more general level, I would like to question the value createSlice()
provides, and whether there should be such a helper in redux-starter-kit
at all. The introduction in the library documentation says:
It does not address concepts like "reusable encapsulated Redux modules"
However, I feel like createSlice()
definitely moves into that territory by bundling multiple Redux entities (reducer, action creators, one selector), and does so in a way that, to me, feels inferior to just writing a module that defines and exports these things separately.
Here are a few issues with the current createSlice()
that don't really exist with the "just write a module" approach:
-
TypeScript issues. The approach of generating action creators implicitly from the
reducers
object keys and case reducer types seems to work for now, but is more fiddly and complex that it would be if the action creators were just defined and typed independently. More importantly, the dynamically named slice selector is impossible to type correctly (which, admittedly, could be fixed by giving the selector a fixed name). -
No custom selectors. There is
slice.selectors.get[SliceName]()
, which suggests that other selectors related to this slice should go there as well. This could certainly be made possible by adding aselectors
option. However, this wouldn't really save any code, or add any convenience, compared to defining the selectors separately. Yet, again, it feels strange to have some selectors attached to slices and some not. It's weird. -
No way to bundle thunk actions, sagas, epics, etc. This is of course out of scope for this library, but it does make the "slice" concept kind of strangely limited - I can bundle reducers, action creators and selectors belonging to the same slice of state, why can't I put my other related "non-core" things there as well? With a JavaScript module, this problem just doesn't occur - I can add whatever export I want.
I think that the extraReducers
option you added is another sign of this awkwardness. It looks like purely an artifact of createSlice()
doing a bit too much; using createReducer()
directly makes this extra concept completely unnecessary.
To summarize, I feel that createSlice()
lies too awkwardly in between "remove a little bit of boilerplate when defining a reducer plus some actions" and "approach to bundling related Redux code" that cannot appropriately serve all needs without effectively becoming a miniature version of the JS module system. I would rather recommend library users to write slices as modules, which doesn't take much more code and has none of the problems mentioned above.
/* counter.js */
// Selectors
export function getCounter(state) {
return state.counter
}
// Actions
export const increment = createAction('increment')
export const decrement = createAction('decrement')
// Thunks, epics, sagas, ...
// Reducer
export default createReducer(0, {
[increment]: (state, { payload }) => state + payload,
[decrement]: (state { payload }) => state - payload
})
src/createAction.ts
Outdated
@@ -18,6 +18,7 @@ export interface PayloadAction<P = any, T extends string = string> | |||
export interface PayloadActionCreator<P = any, T extends string = string> { | |||
(): Action<T> | |||
(payload: P): PayloadAction<P, T> | |||
type: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can type type
as T
here. If the action type is statically known (like when you write createAction('foo')
, then actionCreator.type
will have the literal type 'foo'
instead of the generic string
.
@@ -15,6 +15,9 @@ import { | |||
reducers: { | |||
increment: (state: number, action) => state + action.payload, | |||
decrement: (state: number, action) => state - action.payload | |||
}, | |||
extraReducers: { | |||
"OTHER_ACTION_TYPE" : (state : number, action ) => state + action.payload.count |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line looks as if it was not formatted with Prettier (unneeded quotes, space before the :
). Does your editor run Prettier on TypeScript files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I don't have Prettier configured in my editor at all. Probably something I should set up.
@markerikson instead of creating a new field called const actionMap = actionKeys.reduce(
(map, action) => {
const type = getType(slice, action)
map[action] = createAction(type)
return map
},
{} as any
) to const actionMap = actionKeys.reduce(
(map, action) => {
map[action] = createAction(action) // <- this specifically
return map
},
{} as any
) **Edit: The const reducerMap = actionKeys.reduce((map, actionKey) => {
map[getType(slice, actionKey)] = reducers[actionKey]
return map
}, extraReducers) By removing the So instead of const user = createSlice({
slice: 'user',
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // mutate the state all you want with immer
}
},
extraReducers: {
[counter.actions.increment]: (state, action) => {
state.age += 1
}
}
}) we do const user = createSlice({
slice: 'user',
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // mutate the state all you want with immer
},
[counter.actions.increment]: (state, action) => {
state.age += 1
}
},
}) |
@Dudeonyx : maybe I'm misreading the changes you're suggesting, but it seems like they break part of the point of what I specifically want |
Implemented my proposed changes from #83 (comment) @denisw @markerikson your inputs are appreciated *Note: this PR is for the `feature/other-slice-action` branch not `master`* * ~~Removed `getType()` utility~~ * ~~`slice` is no longer attached to `actionsMap` and `reducerMap`~~ * ~~Removed `reducerMap` and directly forward `reducers` to `createReducer`~~ - [x] `createAction()` action creators `type` property returns a `string literal` for better type saftey - [x] Fixed tests - [x] Added tests
# Conflicts: # docs/api/createSlice.md
Didn't realize #86 was really meant for this branch. Merged it over, and putting this in. |
* Add `type` field to action creators * Update createSlice to handle other actions * Fill out createSlice docs * Formatting * Hacky attempt to fix createAction type tests * Fix example typo * Alternate: Add ability for slices to listen to other actions (#86) Implemented my proposed changes from reduxjs/redux-toolkit#83 (comment) @denisw @markerikson your inputs are appreciated *Note: this PR is for the `feature/other-slice-action` branch not `master`* * ~~Removed `getType()` utility~~ * ~~`slice` is no longer attached to `actionsMap` and `reducerMap`~~ * ~~Removed `reducerMap` and directly forward `reducers` to `createReducer`~~ - [x] `createAction()` action creators `type` property returns a `string literal` for better type saftey - [x] Fixed tests - [x] Added tests * Play with type tests a bit
Thus far, slices generated by
createSlice
could only listen to the specific action types defined for each field in thereducers
option.I've added a field named
extraReducers
that lets you add reducers that listen to action types that were defined elsewhere.I'm open to bikeshedding suggestions on the field name. Other ideas:
I also updated
createAction
so that action creators have the string available as atype
field. This was requested in #72 for use with switch statements. Turns out it also helps with TS usage, because TS won't let you pass a function directly as a computed object property - it's not a string orany
. So,[actionCreator.type]
is a passable alternative.The TS compiler seems to be okay with this at the moment, but this is the first time I've ever done anything meaningful with TS, so I'd appreciate review. (Paging @denisw , @Dudeonyx , and @Jessidhia ...)
I filled out the
createSlice
docs as well.