August 29, 2020 by Charanjit Singh

My recent bout with React/Redux boilerplate

blog-feature-image

With rise of GraphQL, I often have to work with React Apollo. I personally don’t like apollo for reasons I’ll refrain from discussing in this post. But I absolutely loved its useQuery and useMutation react-hooks.

When building SoS App, I chose Redux for state management because:

  1. It gives a greater degree of control and predictability over global state
  2. Its debugging tools are very helpful

But Redux does not come without problems. My biggest beef with Redux is

The Boilerplate Monster

Redux is too verbose. It causes problems, from making learning hard for beginners, to significant loss of productivity. Even causing serious accumulated fatigue over time. You don’t get a good return for your investment.

Internet is full of different approaches to solve this.

@redux/toolkit

@redux/toolkit provides utilities to fight off the boilerplate monster. It’s brilliant, i make extensive use of many of them, but I wanted more. A friction-free way to perform async actions, with as few as possible compromises with Redux principles.

Custom react hooks

React hooks provide a great way of creating modular abstractions which can be beautifully composed together. They give me the warm fuzzy feeling of using Haskell Type Classes.

I chose to create our own version of useQuery, that

  1. interoperates with @redux/toolkit’s createAsyncThunk
  2. optimized ease of use with minimal compromises of redux principles

A Compromise with “The Redux Way”®

I chose to keep state of async operation (i.e loading, success, and failure) locally inside the component. Reason being that most of the times, I don’t care about the lifecycle of async operation globally, just the results.

e.g when fetching list of employees, this is how I want to handle its state transitions:

  1. Loading: show the skeleton in EmployeesList
  2. Error: show error notification
  3. Success: store the employees in global state; and show success notification

Keeping this in mind, I came up with this react-hook:


    const useAsyncThunk = (
      asyncThunk: (args?: any) => AsyncThunkAction<any, any, any>,
      options: Options = {},
    ) => {
      const [isFetching, setIsFetching] = useState<boolean>(false);
      const dispatch = useDispatch();

      const runAsyncThunk = useCallback(async (args?: any) => {
        try {
          setIsFetching(true);
          return await unwrapResult((await dispatch(asyncThunk(args))) as any);
        } catch (err) {
          throw err;
        } finally {
          setIsFetching(false);
        }
      }, []);

      return [runAsyncThunk, isFetching] as [(arg?: any) => Promise<void>, boolean];
    };

This tiny little hook allow us to do this in components;


    const [createEmployee, isCreatingEmployee] = useAsyncThunk(createEmployeeAction);

Ergonomic much!? While we are doing this, Redux is still getting all the right actions. createEmployeeAction here is just a simple async-thunk wrapping a plain TS function which hits an API.

Incidentally, in SoS App we communicate status of async operations with toast notifications. That meant we can get rid of even more boilerplate. With some small modifications, we were able to make calls like this:


    const [createEmployee] = useAsyncThunk(createEmployeeAction, {
      errorTitle: 'Failed to crate Employee',
      successTitle: 'Employee created successfully',
    });

which would:

  1. Perform the async action “the Redux way” (i.e by emitting all the right actions)
  2. Show error/success toast messages on completion

This boosted our productivity considerably. We could just create an async-thunk, and using this hook we could go “just do it” with the async operation, and user feedback was handled for free.

I am pretty okay with this compromise and gains we had from it. But I still feel the Redux way is a lot of work. Redux focuses too much on puritanism which don’t really pay off. Until we find a better way, I hope this little tid-bit might of use for someone else too!

xstate looks very promising. It’s not really a Redux alternative, but something that can make modular components much more robust.

LET’S WORK TOGETHER