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)