The Path to Hell

React's Context API isn't new to the framework, but its inclusion into the Hooks paradigm has further entrenched it into the API. As helpful as it certainly is, without serious thought a team can quickly turn their React code-base into an unmanageable pile of hard-to-track mutations.

The Context API simplifies the solution that "prop drilling" gives us and in a much more ergonomic way. However useful and ergonomic this API is it can spiral out of control when an application gets larger and a development team lacks discipline. The Context API gives us the ability to store and access global variables for which the software community has long frowned on and with good reason. These variables--unless constants--can be mutated god-knows-where and the burden falls on whomever is working in a given section of the code-base to figure out if, and where, it's being done.

The React docs give us an explicit example on how to mutate the Context from a child component. This is fantastic if you're the sole developer. But let's say you're on a team of five to six developers and you're writing a React application for an E-commerce company. You're probably going to need user state of some sort (who they are, what they've purchased, what they might purchase, address, etc). Let's say we have a UserContext like so:

export const UserContext = React.createContext({
  firstName: 'Matt',
  address: '123 Fake St.',
  whatsInMyCart: () => {/* return stuff in cart */},
  addToMyCart: () => {/* add stuff to my cart */},
})

We would, of course, need to pass this to some consumer:

const SomeConsumer = () => {
  const userStuff = React.useContext(UserContext)
  
  return (
    <div>{userStuff.name}</div>
  )
} 

Now suppose we have another consumer somewhere else in the code that mutates one of the values in the UserContext:

const AnotherConsumer = () => {
  const userStuff = React.useContext(UserContext)

  const changeUsersCart = () => {
    setTimeout(() => {
      userStuff.addToMyCart('something')
    }, 3000)
  }

  useEffect(() => {
    changeUsersCart()
  })
  
  return (
    <div>/* more fun below */</div>
  )
} 

This is a contrived example to illustrate a point

How would the developer of SomeConsumer know that AnotherConsumer was mutating that UserContext? What if SomeConsumer depended on UserContext.whatsInMyCart() being a specific value? This would be an exercise in frustration for the the developer of SomeConsumer, assuming a large code-base.

The point of this silly example is to show how mutable global variables can create a nightmare situation for a team and a large code-base. As time passes and new developers come onto the project the tendency to circumvent, rather than refactor out the mutation, will necessitate (due to laziness or lack of time) special logic to guard consumers against other consumers mutating their expected values.

The antidote to this is to disallow mutable values in the Context in the first place. That too, takes discipline, but it is well worth it for the future maintainability of the code. Whether it be enforced in the code review process by more seasoned developers, or in some other way, is a matter for the team to decide, but left unchecked the Context API, while helpful, can create a disdain for the code-base.


Topics