You should have a working knowledge of react, reducers, and typescript to get the most out of this post. I wrote this before finding out about mapDispatchToProps
. I still decided to post anyway in case someone not using Redux wanted to implement it themselves in TS.
The idea is to automatically bind a dispatch to a group of action creators.
So instead of manually dispatching:
import {createAddItemAction} from 'items/actions.ts'
const {dispatch} = useDispatch()
dispatch(createAddItemAction({position: 5}))
It'd be nicer to provide a method that already has dispatch
in its scope:
const {addItem} = useEditor()
addItem({position: 5})
I'll be referring to such a method as a mapped built action.
You might also see them referred to as bound action creators, or mapped dispatch to props. Personally I prefer built actions, it implies they're all good to go.
Sounds simple enough, right? Something like this will do:
const addItem = dispatch => position => dispatch(createAddItemAction({position})
But What if we had multiple action creators, and we were passing these into a context value? A ton of repetition.
The rest of the examples will be in Typescript; if you can think of a better way to type the functions - let me know!
Binding multiple action creators
Starting with a standard action creator
item/actions.ts
export const REMOVE_ITEM = 'REMOVE_ITEM'
export interface RemoveItemAction {
type: typeof REMOVE_ITEM
category: Category
position: number
}
const remove = (args: {in: Category; at: number}): RemoveItemAction => ({
type: REMOVE_ITEM,
category: args.in,
position: args.at,
})
We'll also export a build
function together with the actions that calls a withDispatch
function
item/actions.ts
//... insert, and addNew action creators are also defined here
const remove = (args: {in: Category; at: number}): RemoveItemAction => ({
//...
})
export const build = withDispatch({remove, insert, addNew})
withDispatch
does most of the heavily lifting. Its job is to take an object of action creators, and return an object of built actions.
reducer.ts
// takes a bunch of actions ({remove, insert, addNew})
export function withDispatch<T extends ActionCreators>(actions: T) {
return (dispatch: React.Dispatch<Action>): T => {
const wrapped = {} as any
return Object.entries(actions).reduce((acc, [key, createAction]) => {
acc[key] = (...args: any[]) => dispatch(createAction(...args))
return acc
}, wrapped)
}
}
And we'll define our action types too
reducer.ts
export type Action = CategoryAction | ItemAction | MenuAction
export interface ActionCreators {
[prop: string]: (...args: any[]) => Action
Where the binding happens
We'll bind the dispatch where we use the reducer. I'm using it in a context provider here.
editor/state/index.ts
import {reducer, initialState} from 'menu/editor/state/reducer'
import {build as buildItemActions} from './item/actions'
type EditorContextValue = typeof initialState & {
item: ReturnType<typeof buildItemActions>
}
const EditorContext = createContext(
(undefined as unknown) as EditorContextValue,
)
export function EditorProvider(props: {children: ReactNode}) {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<EditorContext.Provider
value={{
...state,
item: buildItemActions(dispatch)
}}
>
{props.children}
</EditorContext.Provider>
)
}
Don't combine everything into a single context like this by the way, it will trigger unnecessary re-renders. See the solution on Github.
Sending the action
Finally, in our component we can just send the action
import {useEditor} from './state'
export default function MenuEditor(props: {restaurantId: number}) {
const {
item,
categories
} = useEditor()
const category = category[0]
item.remove({in: category, at: 2} // Calling our built action
//...
}
Did I just re-invent the wheel? Sure did, but it's your wheel now too, enjoy!
Top comments (0)