useDispatchMethods - a simpler abstraction over React's useReducer

Published

TL;DR - I wrote a custom hook to make useReducer easier to use. Wanna try it out? It's available on npm.

1
npm install --save @dgca/react-use-dispatch-methods

Wanna jump to the demo? I got you.


For a long time, I wasn't a huge fan of Redux. The learning curve felt bigger than it needed to be, the switch statements felt unnatural (I still have to look up the syntax for those since I use them so sparingly), and I just wasn't a fan of the API. Redux Toolkit addressed most of my issues, but the original way of using Redux wasn't my cup of tea.

Because of this, when React introduced hooks, I was a bit apprehensive of useReducer. I appreciate what it does, but again with the switch statements, and having to write a lot of the boilerplate yourself.

That's why I decided to wrap useReducer in a custom hook that implements what I believe is a simpler way of using it. Meet useDispatchMethods!

So, how does it work?

1
2
3
4
5
6
const [state, dispatch] = useDispatchMethods(
  methods,
  initialState, // optional
  init, // optional
  dependencyArray // optional
);

Arguments

  • methods (Object<string, function>) - This is an object where each value is a function that returns the updated state, and each key is the name of the dispatch method you'll use later on. Each function will receive an object of {state, payload} as its single argument. state is the current state, and payload is the argument that was passed to the dispatch method (more on this in a bit).
  • initialState (?*) - The initial state, same as useState or useReducer.
  • init (?function) - A function to lazily initialize the initial state, same as useReducer (see: React's docs on this).
  • dependencyArray (?Array<*>) - In order to prevent recreating some internal objects on subsequent rerenders, useDispatchMethods uses React's useCallback and useMemo. If you need to change the functions in the methods object, you can pass a dependencyArray and we'll forward those onto useCallback and useMemo. Odds are, you won't have to worry about this.

Return value

  • state (*) - The current state, same as useState or useReducer.
  • dispatch (Object<string, function>) - Alright, here's the fun part. Remember the methods object you passed in earlier? dispatch is an object that has the same keys. In order to update your state, just call dispatch.someUpdateFunction(optionalPayload). When you call a dispatch method (eyy, there's where the hook's name comes from), you're actually dispatching an action and the underlying useReducer's reducer function figures out which update function it needs to use to modify the state!

Demo

So, let's see this in action. First, a demo of our custom hook in use. We're using useDispatchMethods to modify the state that controls the component's background color, and the width of the border. It's ugly, I know.


So, in short, we're defining our update functions by passing an object of functions the first argument. Each function receives an object of {state, payload} as its argument, and it returns the new state.

The color input shows that you can call a dispatch method with a payload (in this example, it's the input's e.target.value). That's the payload that gets passed to the update function. If you don't need to use the payload, you don't have to pass one.

One thing to note is that the update functions must return the entire new state object. In this example, that means cloning the state object by spreading it onto a new object and updating only the property we need to update. You could also use a library like Immer to facilitate this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export default function App() {
  const [state, dispatch] = useDispatchMethods(
    {
      setColor: ({ state, payload }) => ({
        ...state,
        color: payload,
      }),
      increaseBorderWidth: ({ state }) => ({
        ...state,
        borderWidth: state.borderWidth + 1,
      }),
      decreaseBorderWidth: ({ state }) => ({
        ...state,
        borderWidth: state.borderWidth - 1,
      }),
    },
    {
      color: "#fff",
      borderWidth: 10,
    }
  );
  return (
    <div
      className="App"
      style={{
        backgroundColor: state.color,
        borderWidth: `${state.borderWidth}px`,
      }}
    >
      <h1>
        <code>useDispatchMethods</code> demo:
      </h1>
      <label>
        Change background color:&nbsp;&nbsp;
        <input
          value={state.color}
          type="color"
          onChange={(e) => dispatch.setColor(e.target.value)}
        />
      </label>
      <br />
      <br />
      <button onClick={dispatch.increaseBorderWidth}>
        Increase border width
      </button>
      &nbsp;
      <button onClick={dispatch.decreaseBorderWidth}>
        Decrease border width
      </button>
    </div>
  );
}

Neat, right? If you're into this, and you want to use it, it's available on npm, or just grab the code from this Gist and paste it into your project.