Skip to content

Suggestion, possible issues and improvements #41

Closed
@doxick

Description

@doxick

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 with books/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 way createSlice does, without having them tied 1-1 to a reducer.
  • createActions can be re-used within createSlice, but exposes the same functionality without having to create a full slice

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 using createActions, 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions