DEV Community

Cover image for A Better useReducer: Colocating Side Effects With Actions Using useReducer
conor hastings
conor hastings

Posted on • Edited on

A Better useReducer: Colocating Side Effects With Actions Using useReducer

The word colocation gets thrown around a lot these days.

Styles in my mind being the biggest example There seems to be a near constant conversation revolving around colocating styles with components, the argument often being that styles are part of the component and without them it would not be what it is. Putting the two together allows one to reason in a single place about what will appear on the page.

I won't get into that here because I wish to spend most of my time not arguing about styles on twitter.

What I'll talk about instead is React hooks, I believe they have introduced a place where we have an astonishing chance for colocation in data fetching and data flow in general.

I didn't randomly choose to put bread and cheese emojis in the header image because those just so happened to be the two things currently on my mind, I see them as a great example of things that are commonly colocated in this real world everyone keeps telling me about (but I don't get MTV I respond to rapturous laughter).

As hooks gain more and more popularity, specifically useReducer we often begin to see it paired with useEffect in many different sorts of ad hoc ways in fetching data. Something like this contrived example below:

function Avatar({ userName }) {
  const [state, dispatch] = useReducer(
    (state, action) => {
      switch (action.type) {
        case FETCH_AVATAR: {
          return { ...state, fetchingAvatar: true };
        }
        case FETCH_AVATAR_SUCCESS: {
          return { ...state, fetchingAvatar: false, avatar: action.avatar };
        }
        case FETCH_AVATAR_FAILURE: {
          return { ...state, fetchingAvatar: false };
        }
      }
    },
    { avatar: null }
  );

  useEffect(() => {
    dispatch({ type: FETCH_AVATAR });
    fetch(`/avatar/${usereName}`).then(
      avatar => dispatch({ type: FETCH_AVATAR_SUCCESS, avatar }),
      dispatch({ type: FETCH_AVATAR_FAILURE })
    );
  }, [userName]);

  return <img src={!fetchingAvatar && state.avatar ? state.avatar : DEFAULT_AVATAR} />
}

This code, barring mistakes I almost definitely made, should work, but the thought of slogging through this every time I'm doing something like loading an avatar is a nightmare. But I still love the useReducer pattern so whatttttt am I to do.

I believe ReasonReact has already solved for this with the reducerComponent

ReasonReact provides the functions Update, UpdateWithSideEffect, SideEffect, and NoUpdate which are than used to wrap the value returned from the reducer allowing not so much the colocating of side effects with the reducer (which we still want to keep pure) but the colocation of the INTENT of side effects that will follow.

We can take this idea and bring it over to the world of React hooks to give us a somewhat similar experience, remove excessive code like that seen above, and provide a common pattern for teams to execute actions that lead to side effects. Hopefully causing easier understanding of the code, easier to review pull requests, and the actual important reason, less bugs reaching the end user.

Here's what the above code might look like in that world.

function Avatar({ userName }) {
  const [{ avatar }, dispatch] = useReducerWithSideEffects(
    (state, action) => {
      switch (action.type) {
        case FETCH_AVATAR: {
          return UpdateWithSideEffect({ ...state, fetchingAvatar: true }, (state, dispatch) => {
                fetch(`/avatar/${usereName}`).then(
                  avatar =>
                    dispatch({
                      type: FETCH_AVATAR_SUCCESS,
                      avatar
                    }),
                  dispatch({ type: FETCH_AVATAR_FAILURE })
                );
          });
        }
        case FETCH_AVATAR_SUCCESS: {
          return Update({ ...state, fetchingAvatar: false, avatar: action.avatar });
        }
        case FETCH_AVATAR_FAILURE: {
          return Update({ ...state, fetchingAvatar: false })
        }
      }
    },
    { avatar: null }
  );

  useEffect(() => dispatch({ type: FETCH_AVATAR }) , [userName]);

  return <img src={!fetchingAvatar && state.avatar ? state.avatar : DEFAULT_AVATAR} />;
}

We're now able to colocate the fetching of the avatar with our declaration of intent to fetch the avatar, allowing us to follow in one section of code exactly what is happening.

I believe ReasonReact got this super duper extremely correct and am excited to use this pattern with React hooks as I develop new features.

You can see a library implementing this pattern here and to be honest it's not all that difficult to do this or something similar on your own.

If you're interested in hearing more on the topic and are the SF Bay Area I'll be speaking on the topic at the July React Meetup

Looking forward to hearing what everyone thinks!

Top comments (3)

Collapse
 
yakimych profile image
Kyrylo Yakymenko • Edited

Hasn't ReasonReact switched to the React-hooks useReducer API without UpdateWithSideEffect after the latest update? Seems like those old features are "frozen" now: reasonml.github.io/reason-react/do...

The Record API is in feature-freeze. For the newest features and better support going forward, please consider migrating to the new function components.

Collapse
 
stillconor profile image
conor hastings

I did see this, but I liked the pattern anyway, maybe i'm missing something but do they have more complete info on hooks in reason

Collapse
 
yakimych profile image
Kyrylo Yakymenko

I liked it too! I think they just link to the React hooks docs, since they work the same in ReasonReact.