React State: Doing the Least Bad Thing

Isaac Corbrey
The Startup
Published in
4 min readJul 30, 2020
So much caffeine and ibuprofen over such a small thing
Photo by Sebastian Herrmann on Unsplash

Why is state such a hard problem to tackle?

State is a very hard concept to get right, but why is it so difficult? Well, ultimately the problem boils down to the fact that state is just plain old dirty. It’s a necessary evil in software development, and even the cleanest, most perfect stateless code has to hook into this horrible monster at some point. If this is inevitable, how can this pain be avoided?

Let’s start by heading into React land. The way that React encourages clean code is by limiting what code can be written. For example, in React, state is immutable. It cannot be changed on a whim via assignment; instead, it must be completely replaced though the setState() method (or through a state hook if the component is functional).

In addition, there is no way to directly pass data from a child to a parent component. The only way to do this is by passing a callback to the child that can then be called with the data to pass back up. This helps to reinforce the idea of unidirectional data flow, which prevents developers from doing too many messy things with state.

Lifting state

One of the many sticky situations React developers run into is where multiple components need to be able to write to the same state. When each component has its own local state, this is impossible. How can this be resolved? By lifting the state!

In essence, this process involves migrating the state of child components into their parent component. When a child needs to read the state, it’s passed into the child’s props. When it needs to write to the state, the parent passes it a callback that performs the operation when called.

New problems arise: God components and prop drilling

At a small scale, lifting state works very well by itself. However, once the codebase becomes big enough another unfortunate problem rears its ugly head: God components. These components grow like a cancer, absorbing state until it becomes this eldritch horror with an unholy amalgamation of 15+ separate state systems and a severe case of callback purgatory.

Not only are God components bad, but they introduce yet another problem: prop drilling. Since all this state now lives within a single top-level source, data must be drilled down through child props until it reaches its intended target. This clutters child components by introducing props that in no way relate to them, but can’t be removed since their children still need the data.

Using contexts to sidestep the component tree

For this particular set of problems, contexts are almost too perfect of a solution. Contexts live and work alongside your application, and they allow components to access data without having to drill holes deep into the app. They integrate into the app with a special provider component that allows any component underneath it to access its data.

On their own, contexts provide static data at a global level for components to access. However, by creating a new component that wraps the context and manages its state this static source of data can become a fully qualified state container. (Shameless plug: I wrote react-context-stateful for precisely this scenario, go check it out!)

Using stateful contexts also facilitate the separation of different slices of data in the application. This way each component can grab only the contexts it needs — nothing more.

Going one step further by borrowing concepts from Redux

Redux is a library that provides a way to create predictable state containers. This means that when a developer creates a given source of information, they also create a set of predefined actions that can be taken on that information. This is done by defining reducers, pure functions that return a new state based on the previous state and some action. This concept can be used in place of normal state to make our stateful contexts even more powerful.

Using reducers makes our state testable

Since reducers are pure functions, testing their functionality is far easier than trying to test normal state. There’s no special inspection that has to be done to determine what the next state is going to be; rather, we can simply pass a test state and the action we want to test into our reducer and assert that the output state is what we expected!

Functional components make it easier to use reducers as state

Because React provides the useReducer() hook for use in functional components, it’s much easier to integrate reducers into stateful contexts if you define them as functional components. Otherwise, you have to manually try to figure out how to substitute reducers for state in class components, which sounds like a pain.

Why don’t we just use Redux in the first place?

I personally avoid explicitly using Redux as a framework. It’s very boilerplate heavy, resulting in multiple files per data set, and that can stack up very quickly. Plus, in order to use the Redux store you have to wrap every component that uses it in a higher order component, which can get old real quick.

Don’t let contexts be your only tool

As the old saying goes, “If all you have is a hammer, everything looks like a nail.” Diversify your problem-solving toolkit and choose the best tool for the job. If it makes sense to drill a piece of data down into a component, then do that. If it makes sense to create a local state for a component, then do that. If it makes sense to create a stateful context for a wide range of components, then do that. If it doesn’t make sense to use something, don’t use it.

--

--

Isaac Corbrey
The Startup

Always looking for the next cool thing to geek out about, whether it be a new JS framework or yet another batch of RPi Picos coming in the mail.