Mapped Reducer Actions in TS
Tags: react, typescript, redux
Published: 2020-03-30 15:18:04

*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:

```jsx
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:

```javascript
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:

```js
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**

``` 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**

```js

//... 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**

```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**
```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**

```tsx
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](https://github.com/facebook/react/issues/15156#issuecomment-474590693).*

### Sending the action

Finally, in our component we can just send the action

```tsx

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!