You may not need a global god state in React.

An argument and solution for multiple global states

Encapsulate your global React state instances with react-capsule.

Preface: How did we get here? 👀

Having been primarily a React developer for 3 years now, I have thought about and written extensively on state management in React: everything from its performance to its design. In my original deep dive into developer experience, my “No-boilerplate global state management” article, I gathered as many data points as possible for my and other teams’ experiences with using Redux to manage their global state. Redux solves a lot of problems. It is well-designed for both handling these problems and extending support to other features and functionality as needed. You may be familiar with Redux thunks, sagas, or other middlewares that your team has harnessed to meet particular needs.

When assessing difficulty with onboarding Redux, the root cause is primarily that Redux is overkill for most projects. Redux solves problems that do not exist in the application. Because of this, the application’s implementation of Redux requires boilerplate and patterns without a “why” to justify their presence. New developers to the project have had a hard time grasping what was happening, what needed to change, and when it needed to change, because it didn’t make much sense why it was already the way it was. Dan Abramov’s “You might not need Redux” article is the inspiration for this article’s title.

You may be familiar with a similar concept in legacy code. “It works. We don’t know how it works. When we touch it, it breaks. Don’t touch it.” There are pros and cons to systems that operate with legacy code, and there is evidence that shows that old code is safer code. Unfortunately, when it comes to global state, “don’t touch it” is not an option! Your application will grow. Its global state will change. That means new developers are going to have to implement global state. They may be new to programming; they may be new to JavaScript; they may be new to React; or they may be new to Redux. In one way or another, they are going to touch the global state and break everything. Hopefully your unit tests prevent bugs from shipping to production. The real problem here is that a task that takes the experienced team member one day ends up taking the new hire a stressful two weeks.

I created ReactN in response to my team’s and other online developers’ feedback about Redux. I wanted to simplify the integration of and interaction with global state in a React application. It was a successful project, allowing large companies like Amazon and Nintendo to quickly develop and deploy applications. While feedback from new hires regarding the onboarding experience has improved over Redux, there were still a few points where ReactN was overkill. Much like Redux, problems were being solved that weren’t always applicable in the application, resulting in a more complicated API with little benefit to the scenario.

I have come to realize that there can be no such thing as a hypothetical “global god state,” a single source of truth for all state in the application. At least not without a cost that outweighs the benefits. There are and should be single sources of truth. Your address bar should tell you the user’s location, or your window object should tell you about the browser environment. These are great sources of truth, because they are decoupled. They exist in a vacuum and are dedicated to a managing a single state concept. I have been and still am an opponent to replicating global state for the sake of creating a “god state.” A popular occurrence is to replicate the user’s location (URL) into Redux when what the developer really wants is to just re-render a component when the URL changes. Coupling global states together does not always make sense, and it creates difficulty in extensibility, maintenance, and conceptualization of the application and its relation to global state. It creates a complicated API for what should be simple states. Having your API asynchronously requested and mapped to props is good, but now sharing that complex global state setup with your URL doesn’t make any sense. Should I copy-paste your saga boilerplate to update the URL? Or to increment a counter? Do I need to write a mapStateToProps function just to figure out what the URL is or read a numeric value directly? The answer should be no to both of these, but a “global god state” encourages this code smell. Over time, it becomes increasingly difficult to manage any one slice of global state, and I’ll provide examples later in this article.

Global states — or shared states between components — should exist in a vacuum. There can be any number of them. When they do not relate to each other, they should not be forced to relate to each other.

Problems

I will briefly cover some problems I have seen in enterprise production code as a result of a global god state object. I want to emphasize both enterprise and production here. These problems were encountered by very intelligent and educated people. These developers have created some of the most advanced systems in the world, far beyond what I myself am capable of doing. The following difficulties in maintenance still arose, turning simple feature requests into monstrous tasks that slowed development to a crawl. These products living in production means that delays directly impacted customers and shortcuts would encourage bugs.

The following problems heavily overlap, so you will see some repeat statements. Difficulty in mocking is related to difficulty in refactoring. Difficulty conceptualizing is related to difficulty mocking. I believe seeing how they relate, even if it calls for repetition, paints a fuller picture and drives the point home.

Refactoring

A global god state object was originally designed to track API responses (e.g. a list of users). For these API requests, the API status (such as loading or error) was tracked in the local component state. That worked great when only one component was making API requests. Eventually, multiple components were mounted simultaneously that needed access to that API response, and they all needed to track the API’s status in order to not duplicate the request. Refactoring the global state to include the API status was too time-consuming of a task due to all the touch points that the global state has on components and the asynchronous state updates integrating with the synchronous component mounts.

As a result, and the inspiration for this reevaluation of global state management, the team wanted to create a concept of global state that had a smaller blast radius. These API statuses were stored in ECMAScript modules, which have a blast radius limited to the few consuming files. This was very easy for developers to conceptualize, understand, refactor, test, and extend. The only thing missing was tying state changes to the React component’s life cycle, which I will cover later.

Mocking

When writing unit tests, mocking the entire global state is a tedious process. Adding a single property to the global god state object requires mocked states to be updated in countless files.

One could argue that access to the global state can be abstracted away: Instead of mocking the entire global state in order to useGlobalState, all components could access a dedicated entry point for the subset of state desired, e.g. useTodoListItems. I would agree. Unfortunately, what you should do and what gets done are two different things, especially on large teams or cross-team efforts where not everyone is as equally educated on best global state practices, React hooks, or even JavaScript. Reality and theory are often two different beasts; in reality, developers mock the entire global state for their unit tests, because their components are directly consuming either the entire global state directly or its store, causing massive app-wide test failures when the global state changes. When the setup code for one component test was written by one developer and differs from the setup for another, the refactor is intense and time-consuming.

I reemphasize that global states unrelated to each other should not be forced to relate to each other by sharing a global god state object. Adding or removing state should only impact consumers of that state, which is a practice that is difficult to conceptualize and maintain for global god state objects. A mechanism needs to exist that encourages separate global state objects for each unrelated slice. Mocking a slice of global state should be possible, easy to do, and easy to understand, even for new hires with little onboarding needed.

Conceptualization

When the global state values have absolutely no relation to each other, there is no reason for them to share scope. My list of users does not need to know if dark mode is enabled and vice-versa. A great benefit of Redux is using multiple state values when processing actions and mapping state to props, but that is not always needed. When it’s not, the overhead becomes burdensome. The boilerplate becomes difficult to understand. The code has no functional purpose. Since understanding is never achieved, implementation becomes copy-pasting difficult-to-understand boilerplate out of fear of breaking a delicate system. A lack of comprehension breeds low confidence and slower feature delivery.

Relation between state values should be clearly defined. A global god state object gives no hint to the relation between two properties. A clearly defined relationship between global states and each other, or global states and their consuming components, makes onboarding, refactoring, and hand-offs easier to understand and maintain.

Performance

When global state properties in the context API share a single object, you create unnecessary rerenders for consumers of that context. My child component may only be using property a of state { a, b }, but it will needlessly rerender whenever property b changes. To address this, popular global state managers like Redux and ReactN pass a constant “store” reference via context, then use throw-away local states to trigger re-renders.

If you are home-rolling your global god state object to alleviate a state manager’s boilerplate, you may be sacrificing performance.

All right, then. Keep your secrets.
All right, then. Keep your secrets.

If you are using a state manager, its context abstraction offers some nice behind-the-scenes “it just works” magic, but it decreases comprehension of how the application works. Feedback shows that abstraction is nice when the API tells you what is happening; but when it comes to React life cycle performance, new developers to the project are unaware of what is being abstracted and why.

Separating states clearly defines when, where, and why components re-render.

Context hell

You may not need the context API if you are not mounting multiple instances of the context. When using the context API, splitting global state properties so as not to share a “god object” creates “Context hell,” a layer of countless contexts wrapping your React application. This very accurately resembles indention or callback hell. Your application can be easier to understand and maintain by not using tools to solve problems you do not have.

Comprehension

Junior React developers, and those still in the process of migrating from class components, have rightful trouble understand the magic behind React hooks. I love hooks, but time and time again the onboarding difficulty of both newer and older developers for React projects has been “How the hell does context work?” and “Where the hell does the state live?” The answer to both of these questions needs to be clear. Removing context helps if context is not solving a problem for you. An ideal implementation will have an exact line you can point to and say “the state lives here.” For Redux, that’s the store. Unfortunately, Redux encourages a global god state, and we will be wanting to migrate to multiple global states. I will cover the ideal implementation later. An ideal implementation should provide an object-oriented approach familiar to many developers, and provides methods and properties for interacting with it directly.

Solutions

The magic of abstraction has a time and place to make complicated tasks easy to perform and maintain. Sometimes, for your global state, the context API or global god objects are not the right and needed abstractions. Sometimes these abstractions make it harder to understand what should be a simple task.

When the context API does not solve a problem you face, consider removing that abstraction. When global states sharing knowledge of each other does not solve a problem you face, consider removing the concept of a god state.

Given the feedback gathered from countless new hires, transitioning team members (from both back end or other front end teams), and experienced developers; and given solutions and ideal conceptualizations provided by the aforementioned parties; the ideal solution for this use case has been encapsulated state.

I want to repeat: the solution for this use case. Encapsulated state won’t replace the context API. It won’t replace the global god state object. It won’t replace Redux or ReactN. Like the user’s address bar and window object, encapsulated state serves as a different kind of global state for optimally managing a single source of truth for specific types of global state. React developers need to move away from the concept of “one global state” and embrace different global solutions for different problems, even within the same application.

I published react-capsule as the solution discussed in this article — a global state instance that exists in a vacuum, has a single entry point, couples directly to the consuming component (and no others), and does not abstract away complex concepts like the React life cycle. Sitting at under 1 kilobyte, the minimalist implementation is easy to implement, understand, and maintain. It is a simple object that triggers a re-render to consuming components when its state changes.

Let’s see it

Creating a new encapsulated state with an OOP interface is simple. In fact, it’s more simple than extending an existing global god state object, and that’s the intention.

Show me the read/write

The package comes with a useCapsule hook that behaves exactly like React.useState, but the value is shared across all components consuming that capsule. It becomes clear how to read and mutate the value, where they state lives, and when, how, and why a developer would change it.

Read more

You can see the full API of the capsule object on the react-capsule NPM page.

Conclusion 🔚

If you have any questions or great insight into global state management, please leave them in the comments below.

To read more of my columns, you may follow me on LinkedIn and Twitter, or check out my portfolio on CharlesStover.com.

Senior front end engineer / charlesstover.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store