Dev /state-and-u… dev/state-and-u… Translation address: github.com/xiao-T/note… The copyright of this article belongs to the original author. Translation is for study only.


To those of us who have experience with Redux, useReducer seems more complex and unnecessary. Between useState and context, it’s easy to fall into a mental trap because reducer adds unnecessary complexity for mostly simple usage scenarios; However, it turns out that useReducer can make state management much easier. Let’s look at an example.

As with my other articles, the code comes from my booklist project. In this project, users are allowed to scan books on the screen. The ISBN is logged, and then more information is queried through a speed limiting service. Because the query service is limited, this does not guarantee that books can be quickly query, therefore, need a webSocket service; Once the data is updated, it is passed through WS, and then the UI is processed. The WS API is very simple: the packet will have a _messageType attribute, and the rest of the information. Obviously, a serious project would be much more robust.

In the class component, the code to start WS is pretty straightforward: WS will be created at componentDidMount, and then turned off at componentWillUnmount. Therefore, it is easy to fall into the trap of using hooks.

const BookEntryList = props= > {
  const [pending, setPending] = useState(0);
  const [booksJustSaved, setBooksJustSaved] = useState([]);

  useEffect((a)= > {
    const ws = new WebSocket(webSocketAddress("/bookEntryWS"));

    ws.onmessage = ({ data }) = > {
      let packet = JSON.parse(data);
      if (packet._messageType == "initial") {
        setPending(packet.pending);
      } else if (packet._messageType == "bookAdded") {
        setPending(pending - 1 || 0);
        setBooksJustSaved([packet, ...booksJustSaved]);
      } else if (packet._messageType == "pendingBookAdded") {
        setPending(+pending + 1 || 0);
      } else if (packet._messageType == "bookLookupFailed") {
        setPending(pending - 1 || 0);
        setBooksJustSaved([
          {
            _id: "" + new Date(),
            title: `Failed lookup for ${packet.isbn}`.success: false},... booksJustSaved ]); }};return (a)= > {
      try {
        ws.close();
      } catch(e) {} }; } []);/ /...
};
Copy the code

We start ws in useEffect, set an empty array as an update dependency, that is, it never fires again, and then we return a function to shut down ws. When the component is first mounted, my WS will be started, and when the component is unmounted, WS will be shut down, just like a class component.

Problem analysis

There is a serious problem with the code. We access state in the useEffect closure, but there is no state in the dependency list. For example, Pending in useEffect is always 0. Of course, we can call setPending in ws-onMessage, which will cause the state to be updated and then the component to be re-rendered, but my useEffect does not refire when it is re-rendered (because its dependency list is empty). This causes the pending value in the closure to remain unchanged.

To be clear, this can be easily discovered with the hooks rule. Fundamentally, it’s important to break the habit of using class components. Do not get the dependency list from componentDidMount/componentDidUpdate/componentWillUnmount. With class components, we only start webSocket once at componentDidMount, which doesn’t mean we can directly convert to useEffect with an empty dependency list.

Don’t overthink it or be too smart: any useEffect values need to be added to the dependency list, including props, state, and so on.

The solution

Of course, we can add state to the useEffect dependency list, and each update will cause the webSocket to restart. This is not very efficient and would cause problems if, in WS, we sent an initialized value because some of the states have already been processed and updated to the UI.

If we look closely, maybe we’ll see something interesting. Each of our operations is based on the previous state. We always say, “Increase the number of books,” “This book has been added to the list,” and so on. In fact, this is reducer; In fact, the objective of the Reducer is to generate new states based on the previous states by commands.

Move the management of the entire state to the Reducer, which will eliminate internal useEffect references to local state; So let’s see how we do that.

function scanReducer(state, [type, payload]) {
  switch (type) {
    case "initial":
      return { ...state, pending: payload.pending };
    case "pendingBookAdded":
      return { ...state, pending: state.pending + 1 };
    case "bookAdded":
      return {
        ...state,
        pending: state.pending - 1.booksSaved: [payload, ...state.booksSaved]
      };
    case "bookLookupFailed":
      return {
        ...state,
        pending: state.pending - 1.booksSaved: [{_id: "" + new Date(),
            title: `Failed lookup for ${payload.isbn}`.success: false
          },
          ...state.booksSaved
        ]
      };
  }
  return state;
}
const initialState = { pending: 0.booksSaved: []};const BookEntryList = props= > {
  const [state, dispatch] = useReducer(scanReducer, initialState);

  useEffect((a)= > {
    const ws = new WebSocket(webSocketAddress("/bookEntryWS"));

    ws.onmessage = ({ data }) = > {
      let packet = JSON.parse(data);
      dispatch([packet._messageType, packet]);
    };
    return (a)= > {
      try {
        ws.close();
      } catch(e) {} }; } []);/ /...
};
Copy the code

Sure, it’s a bit much code, but we don’t have multiple update functions anymore, my useEffect is simpler and readable, and we don’t have to worry about using old state in closures; All updates are made via the Dispatch Reducer. This is also good for testing, our Reducer is extremely easy to test; It’s just a native JavaScript function. As Sunil Pai explained in the React Team, using reducer helps to distinguish between reads and writes. For now, our useEffect will simply focus on the Action of dispatch and produce a new state; Until then, we need to focus on both reading and writing for state.

You may have noticed that instead of using objects, we pass the action through an array, with type in the first place, instead of using the object attribute type. It could be either way; This is just Dan Abramov showing me a technique for reducing code.

To prevent errors

As I mentioned above, the React team has already created lint rules to help us catch errors, with short reminders in the code. It’s here, and it works fine – it’s good for catching errors in your code.

To explore the setState ()

And finally, you might wonder, why didn’t I do this before

setPending(pending= > pending - 1 || 0);
Copy the code

but

setPending(pending - 1 || 0);
Copy the code

This will solve the closure problem, and it works fine in the current demo; However, updating booksJustSaved requires access to Pending and vice versa. This scheme is no longer valid and we need to start from scratch. Moreover, I found that the reducer plan was clearer and the state management was all in the Reducer function.

All in all, I think useReducer() usage is extremely low right now. It’s not nearly as scary as you think. Give it a try!

Happy coding!