A simple asynchronous alternative to React's useReducer

Tags: react, typescript, hooks, asynchronous
Published: 2021-07-27 20:39:44

<span style="background-color: transparent;">Even though React's</span> <span style="font-size: 16px; background-color: #f2f2f2;">useReducer</span> <span style="background-color: transparent;">has gained a lot of popularity during the last couple of years, it can be difficult to use for some common cases. Specifically, it requires a lot of boilerplate to support async actions.</span>

Sure, there are multiple ways of performing side effects/ async actions with <span style="font-size: 16px; background-color: #f2f2f2;">useReducer</span> such as using a <span style="font-size: 16px; background-color: #f2f2f2;">useEffect</span> or maybe making use of other libraries that extend the useReducer hook, either by depending on [<span>thunks</span>](https://www.npmjs.com/package/react-hook-thunk-reducer) or [<span>async action handlers</span> ](https://www.npmjs.com/package/use-reducer-async)to support such functionality.

![Friends keep it simple gif](https://lh3.googleusercontent.com/OpT83Nn48hDbeIL7tMFvC_rJ8yrhQo6ZBO_hW1Y-ihqnGH3c_e47YrBWi3gr9tH9igcyczmREjlsUCfF_OaUT_s3Pin7Px9QHw5pH8yevh5nfeJ3QbRvuK-hvxJjbWCA3vZD4KzY)

But there is always a simpler and better way. [`useSimpleReducer`](https://www.npmjs.com/package/@bitovi/use-simple-reducer) offers an approach that is more intuitive and less verbose making it easier to create asynchronous actions.

Use it today by installing it from its NPM package.

`npm i @bitovi/use-simple-reducer`

Or try a working demo [here](https://codesandbox.io/s/quizzical-hermann-wtx8j?).

## The problems use-simple-reducer solves

There are multiple benefits of using useSimpleReducer over useReducer:

*   Easy to create async actions
*   Less boilerplate code
*   Error handling and recovery
*   Built-in type checking

## Easy to create asynchronous actions

One of the most common patterns in front-end development is to:

*   Asynchronously update the server upon some user action (ex: clicking a button)
*   Show that the server is being updated (ex: a spinner or a disabled action button)
*   Show the updated state when the action completes.
*   Return an error if the async action fails

A simple case is a counter.  You want your JSX to look like this: 
```typescript
        <div>
          <button onClick={() => add(2)}>Add</button>
          <div>
            <p>Steps: {count}</p>
            <div>{isActive ? <Loader /> : "Processing completed"}</div>
            {error && <p>Error: {error}</p>}
          </div>
        </div>
```
Where:

*   `add` **async updates** the server
*   `isActive` displays a spinner while the **action is running**
*   `count` will be updated when the **state changes**
*   `error` will be of a non null value if the **async action failed**

**BUT … this is HARD with useReducer**

A useReducer implementation  might look something like:
```typescript
    type ActionType =
      | { type: "LOADING" }
      | { type: "ADD_SUCCESS", payload: number }
      | { type: "ADD_FAILURE", payload: any };

    type StateType = {
      count: number,
      isActive: boolean,
      error: any,
    };

    const initialState = {
      count: 0,
      isActive: false,
      error: null,
    };

    function Counter() {
      const [{count, isActive, error}, dispatch] = useReducer(
        (state: StateType, action: ActionType) => {
          switch (action.type) {
            case "LOADING":
              return {
                ...state,
                isActive: true,
              };
            case "ADD_SUCCESS":
              return {
                ...state,
                count: state.count + action.payload,
                isActive: false,
                error: null,
              };
            case "ADD_FAILURE":
              return {
                ...state,
                isActive: false,
                error: action.payload,
              };
            default:
              return state;
          }
        },
        initialState
      );

      const add = (amount: number) => {
        dispatch({ type: "LOADING" });
        // An api call to update the count state on the server
        updateCounterOnServer(state.count + amount)
          .then(() => {
            dispatch({ type: "ADD_SUCCESS", payload: amount });
          })
          .catch((error) => {
            dispatch({ type: "ADD_FAILURE", payload: error });
          });
      };
      return (
        <div>
          <button onClick={() => add(2)}>Add</button>
          <div>
            <p>Steps: {count}</p>
            <div>{isActive ? <Loader /> : "Processing completed"}</div>
            {error && <p>Error: {error}</p>}
          </div>
        </div>
      );
    }
```
This is much more simple with useSimpleReducer:
```typescript
    type CounterState = { count: number };

    const initialState = {
      count: 0,
    };

    function Counter() {
      const [state, actions, queue, error] = useSimpleReducer(
        // initial state
        initialState,
        // collection of reducer methods
        {
          async add(state: CounterState, amount: number) {
            // An api call to update the count state on the server
            await updateCounterOnServer(state.count + amount);
            return { ...state, count: state.count + amount };
          },
        }
      );
      return (
        <div>
          <button onClick={() => actions.add(2)}>Add</button>
          <div>
            <p>Steps: {state.count}</p>
            <div>{queue.isActive ? <Loader /> : "Processing completed"}</div>
            {error && <p>{error.reason}</p>}
          </div>
        </div>
      );
    }
```
<span>Looks quite a bit cleaner, right?  Here is why:</span>

1.  <span>Logic is now encapsulated in separate methods, rather than in one giant `switch` statement. Instead of having to extract a `payload` from our action object, we can use simple function parameters.</span>  
    <span></span>  
    <span></span>
2.  <span><span></span></span>Instead of getting back a one-size-fits-all `dispatch` <span style="background-color: transparent;">function, we get back a set of callbacks</span> `actions`<span style="background-color: transparent;">, one for each of our "actions". </span>

<span><span></span></span>**And you get queuing,  error handling, and type checking for free!**

<div class="hs-embed-wrapper" data-service="codesandbox" data-responsive="true" style="position: relative; overflow: hidden; width: 100%; height: auto; padding: 0; max-width: 1000px; max-height: 500px; min-width: 256px; display: block; margin: auto;">

<div class="hs-embed-content-wrapper">

<div style="position: relative; overflow: hidden; max-width: 100%; padding-bottom: 50%; margin: 0px;"><iframe width="1000" height="500" src="https://codesandbox.io/embed/gifted-elbakyan-g31c3" style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; border: none;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe></div>

</div>

</div>

### Queueing

<span>Instead of dispatching actions, the user can use the `actions` value to call the reducer methods provided.</span>

Any invoked reducer action gets added to a queue. The queue will then start processing those asynchronous actions in the same order they have been added.

An `queue.isActive` flag indicates whether the queue is currently processing any actions or not.

A set of values `queue.runningAction` and `queue.pendingActions` are also exposed that can be used for debugging the current state of the queue.

### Error handling

The `useSimpleReducer` hook returns an `error` if any of the reducer methods fail.

This error object exposes a number of recovery methods that provide the flexibility for the user to run the failed action, pending actions, or all of them.
```typescript
    return (
        <div>
            <button onClick={()=> actions.add(2)}>Add</button>
            <div>
                <p>Steps: {state.count}</p>
                <div>{queue.isActive ?  : "Processing completed"}</div>
            </div>
            {error && <AlertDialog content={error.reason} onConfirm={() => error.runFailedAction()} />}
        </div>
    );
```
An in-depth explanation of these values can be found in the API documentation on [Github.](https://github.com/bitovi/use-simple-reducer/tree/main)

### Final thoughts

<span>I know it's a very common pattern in the industry to use a </span>`useReducer`. But I believe that `useSimpleReducer` does it better in a way that is more intuitive to understand while offering extra capabilities.  

You can try out the [demo](https://codesandbox.io/s/quizzical-hermann-wtx8j?) or install the package from [NPM](https://www.npmjs.com/package/@bitovi/use-simple-reducer).