Async actions with useReducer in React

What about async actions?

React's dispatch function only knows how to synchronously dispatch a plain action object.

What if we want to dispatch a function with some async logic inside?

// ❌ You can't dispatch a function
dispatch(someAsyncAction()).then(...);

// ✅ You can only dispatch an object
dispatch({ type: 'FETCH_FINISHED', data });

Creating a custom hook

We are going to create a custom React useReducer hook that knows how to handle functions.

It will allow us to dispatch async actions along with regular actions.

import { useReducer, useCallback } from 'react';

function useReducerWithThunk(reducer, initialState) {
  const [state, dispatch] = useReducer(reducer, initialState);

  function customDispatch(action) {
    if (typeof action === 'function') {
      return action(customDispatch);
    } else {
      dispatch(action);
    }
  };

  // Memoize so you can include it in the dependency array without causing infinite loops
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const stableDispatch = useCallback(customDispatch, [dispatch]);

  return [state, stableDispatch];
}

export { useReducerWithThunk };

Similar to redux-thunk, if the dispatched action is actually a function, the hook calls that function and passes dispatch as an argument. Otherwise, it's treated as a regular object action.

We use useCallback to make our custom dispatch function stable (similarly to the regular dispatch function). It won't change on re-renders (important if you pass it to useEffect dependencies list).

Using the hook

Now you can write thunk action creators that return a function instead of an action object:

// Thunk function
function fetchUserAction() {
  return async function fetchUserThunk(dispatch) {
    const user = await fetchUser();
    dispatch({ type: 'FETCH_SUCCESS', payload: user })
  }
}

Then you can use that hook like this:

import { useReducerWithThunk } from './useReducerWithThunk';

function User() {
  const [state, dispatch] = useReducerWithThunk(reducer, '');

  function handleClick() {
    dispatch(fetchUserAction());
  }

  return (
    <>
      <button onClick={handleClick}>Fetch user</button>
      <p>User: {state}</p>
    </>
  );
}

Source code is available on codesandbox.