Description
based on my feedback on your reddit post, i tried to split out my comment in Improvements, suggestions and (possible) Issues
Improvements
Allow actionCreators from createAction to shape their payload
The current implementation of createAction
returns an actionCreator which takes 1 argument: the payload.
If you want the payload to support multiple parameters (userId, userName, for instance), you'll have to manually recreate the object every time you use the actionCreator:
const updateUser = createAction('USER_UPDATE')
...
dispatch(updateUser({ userId: 10, data: { firstName: 'martijn', lastName: 'hermans' } }))
Will result in:
{
type: 'USER_UPDATE',
payload: {
userId: 10,
data: {
firstName: 'martijn',
lastName: 'hermans'
}
}
By giving createAction
a second parameter, which reduces the arguments passed to the actionCreator into a payload, you generate actionCreators which allow for multiple arguments.
const updateUser = createAction('USER_UPDATE', (userId, data) => ({ userId, data }) )
...
dispatch(updateUser(10, { firstName: 'martijn', lastName: 'hermans' })))
This will result in the same action, yet it becomes more re-usable, predictable and testable.
The default behaviour, without a "payload reducer" stays the same as it currently is: The first argument becomes the payload.
A basic implementation of this could be:
const identity = payload => payload
export function createAction(type, payloadReducer = identity) {
const action = (...args) => ({
type,
payload: payloadReducer(...args)
})
action.toString = () => `${type}`
return action
}
A more advanced implementation can be found in redux-actions/createAction.js
Exposing the slice Name/Type through .toString()
Exposing the slice 'key' has multiple benefits:
state[slice]
will return the part of the state the slice operates on
createActions(slice, ....)
provides the same prefix as the slice has given to the actions
combineReducers({
[userSlice]: userSlice.reducer
})
you can now change the key within createSlice
, and your whole app will function exactly as it did.
In my current implementation, the reducer returned from createReducer
has a type: createReducer(type, actionsMap, initialState)
.
This is basically what the current createSlice(...).reducer
does, but with the added benefit of re-using it as a constant key for its "domain". (usersReducer.toString() === 'books', etc)
Suggestions
createActions(prefix, actionMap)
In it's current state, createSlice
allows you to create prefixed actionCreators.
These actions are however tied to the slice-reducer.
To create custom actionCreators which take the same prefix, you have to manually do that with createAction('users/add')
Prefixing actions has multiple functions:
- Easier to group actions, which prevents collisions:
users/add
doesn't interfere withbooks/add
- Easier to debug: the "domain"/slice which the actions belong to is visible in the
type
- The addition of
createActions(prefix, actionMap)
allows you to create prefixed actions the same waycreateSlice
does, without having them tied 1-1 to a reducer. createActions
can be re-used withincreateSlice
, but exposes the same functionality without having to create a fullslice
The return type of createActions
can either be an
- Object: the actionCreators mapped to the keys
- Array: the actionCreators returned in order of the actionMap
From doing +- 15 projects usingcreateActions
, the Array version is easier to destructure and export
Return-type: Object
export const {
add: addUser,
update,
'do-something-else': doSomethingElse
} = createActions('users', {
add: (data) => ({ data }),
update: (userId, data) => ({ userId, data }),
'do-something-else': (userId, firstName) => ({userId, data: { firstName }})
})
export const {
update
} = createActions('books', {
update: (bookId, data) => ({ bookId, data })
})
- not every action type lends itself for a variable name
- unless you export the complete returned Object, you'll probably rename every destructured variable. (
add
=>addUser
) - destructuring the object version leads to naming collisions if done in the same file (users.
update
vs book.update
Return-type: Array
export const [
addUser,
updateUser,
doSomethingElse
] = createActions('users', {
add: (data) => ({ data }),
update: (userId, data) => ({ userId, data }),
'do-something-else': (userId, firstName) => ({userId, data: { firstName }})
})
- the resulting Array can't be exported without destructuring (unless you want to use actions by their index... not very likely)
- saves boilerplate when destructuring compared to the Object-version
(Possible) issues
actionCreators from createAction have no validation/type safety for the payload
Since the payload is whatever the first argument is i put in the actionCreator, i can easily mess up the data i put in there.
Code-completion doesn't hint what needs to be added.
Testing is not possible, since the input is the literal output for the payload
I have to repeat the same object shape every time i dispatch the action. Not very DRY
Creating actions in slices can lead to collision
The 'type' of the slice optional.
If i make two slices, both with no type, and both with an add
action, the type of both actionCreators is add
, which will lead to naming collision.
// books-slice.js
export const { actions } = createSlice({
reducers: {
add: (state, {payload}) => [...state, payload]
}
})
// users-slice.js
export const { actions } = createSlice({
reducers: {
add: (state, {payload}) => [...state, payload]
}
})
// some-component.js
import {actions: UserActions} from './users-slice'
...
dispatch(UserActions.add(user))
// i now have added both a user and a ... book?
When testing either reducer, you will not run into any issues.
When testing the combined Reducer, you will.
You'll probably also not check your sliceReducer again since that just passed a test and works correctly.
The cause of the error will be very hard to find since the implementation is abstracted away.
Create actions which aren't handled by a reducer for a slice
Since a slice roughly gets treated as a "domain", it would make sense to be able to define actions in createSlice
which don't have a direct connection with a reducer, for instance to catch that action in a middleware.
_Theoretically, you can do that by just adding them with a state => state
reducer
You do want to have actions within the same domain to have the same prefix, since that just makes it easier to debug (and more consistent)
I can't add my custom actions to be reduced by a slice-reducer
Theoretically, i can, by wrapping the slice reducer in another reducer, etc.
Not every action will be created within a slice reducer, yet i will possibly want those handled by the slice reducer. Currently, this is not possible.
Not being able to add custom actions to a slice Reducer or creating non-handled actions in a sliceReducer means i will have to mix different mindsets.
I will expose actions from a slice, but also have to create them separately somewhere else. This results in having to import them from 2 locations, which means actions are now coupled with slices.
I will create reducers with Slices but if i want to handle actions from userSlice in bookSlice, i can no longer create bookSlice but have to write the reducer manually again. There is no way to let bookSlice have a reducer which handles actions from userSlice.
I hope this provides a bit more structured version of my Reddit comment.