No-boilerplate global state management in React
When your React application reaches a certain size and scope, attempting to manage state within component instances adds too much complexity, prop drilling, and code smell. Developers inevitably turn to global state management tools, such as MobX or Redux, to solve these problems and make their lives simpler. I strongly endorse Redux and use it in my personal projects, but not all developers share my sentiment.
I have worked on quite a few large projects that have demanded a global state powerhouse behind the React UI. No matter application size, team size, or member seniority, the almost universal opinion of these global state management packages has been overwhelmingly negative.
The top two complaints? Boilerplate and learning curve. While these packages solve a lot of problems and solve them well, it is not without cost. Developers are not happy with how many files, code blocks, and copy-pasting is required to set up or modify their global states. More importantly, junior developers have a hard time overcoming the learning curve required of them. Creating a global store is a nightmare for some, and extending it with functionality, such as developer tooling and asynchronous features, was a task that took too much company time and caused too many employee headaches.
I polled many developers to gauge their top complaints when integrating global state management into their React applications. You can skip to the end of the list if you don’t want to read them all. These excerpts are merely outline common difficulties when integrating global state into React applications and barriers faced by real React developers.
- “Newer developers may require a longer ramp-up time along with proper training.”
- “New developers have a problem with flux architecture and functional concepts… They should essentially be producing events that describe how the application changes instead of imperatively doing it themselves. This is vastly different than the more familiar MVC-esque patterns.”
- “I found trying to manage a complex state tree in Redux very challenging and abandoned it early on for my app. I really struggled to understand what best practices are outside of simple to-do app examples. I just never really understood how to use Redux in a real-world app with a complex state.”
- “It often feels tedious to do trivial state changes.”
- “It takes junior developers some time to get their head around the magic of autoruns, reactions, etc. Debugging becomes harder when you have to step through MobX code in order get to your own.”
- “It’s annoying that Redux does not handle asynchronous actions out of the box. You have to spend a day figuring out this basic and essential use case. You have to research thunks and sagas. Then, you still have to figure out how to tie them in with actions. It’s a lot to deal with and makes you wish for good old Promises.”
- “For Redux, I dislike that it creates a side-effects vacuum, which has to be filled by a bunch of middlewares. There is a problem in that none of the middlewares out there are perfect.”
- “Whenever I use Redux, I ask myself, ‘What on earth was I thinking?’ It overcomplicates everything. Some would argue that the benefit of Redux is that you can choose the features you need (immutable, reselect, sagas, etc.); but in the end, you’ll be adding all of these to every project anyway.”
- “Redux requires a ton of files to establish a new reducer. A lot of the advantages in practice do not tend to be worth the disadvantages.”
- “Redux has too much boilerplate, and I have to maintain all of it.”
- “You really need to use decorators for MobX. The non-decorator syntax is not nice, and it’s a big dependency.” MobX currently weighs in at 47kB.
- “Redux requires a ton of tedious code to do the most basic things: declare your action name in your action file, create a saga file, add it to your root sagas, create your action generator to call the saga, connect your component to Redux so it can access the store, write mapStateToProps which calls a selector, write your selector to get your user info out of the store, write a mapDispatchToProps so you can dispatch an action in your component, dispatch an action in your component’s componentDIdMount, add an action to handle the result of your network request, write a reducer that saves the user info to the store, add another action to handle an error, add another action to handle loading state, write selectors and reducers for the error and loading actions, call your selector in the render function of your component to get and display the data. Does that seem reasonable for a simple network request? Seems like a pile of hot garbage to me.” While I’m not as experienced with sagas, I will plug my methodology for handling API requests with redux thunk.
- “Global state packages are very cumbersome and complex to set up. They violate the KISS principle — Keep It Simple, Stupid.”
After this list, I feel the need to reiterate: I am a fan of Redux, and I use it on my personal projects. The purpose of this article is not to trash Redux or MobX, or to propose that they are flawed systems. It is to highlight a real issue: There is difficulty integrating these packages into real applications, and most of this difficulty seems to stem from the learning curve. These packages are “too clever” and are not as accessible to junior developers, who tend to make up the majority of contributors to projects.
One piece of feedback I received explicitly blamed the users of the packages: “Users don’t put enough effort into evaluating their needs; don’t use [the packages] judiciously or as advised; don’t give a second thought to any dependencies they add; and never revisit their design decision, then complain about them.” I think they were onto something. I don’t think Redux or MobX are innately flawed, but I think there is a real difficulty in integrating them into enterprise projects. They may not be the best solution, not out of function, but out of complexity.
I am hoping with the release of React 16.7 Hooks and its re-conceptualization of how a readable React application looks, we will see global state solutions that harness creative new methods that appeal to wider audiences. With the ultimate goal of no boilerplate and intuitive syntax, this article will offer my opinion on how a global state management system for React can be structured and finally my open-source attempt of that implementation.
Keep It Simple, Stupid 💋
An Intuitive Approach
My personal take on the matter is that global state management systems appear to be designed with global state management in mind, not React. They are designed so broadly that the intention is to be usable even outside of React projects. That’s not a bad thing, but it is unintuitive for junior developers who may already be overwhelmed by learning React.
React has state management built-in —
this.setState, and the new
useState hook. I posit that global state management should be as simple as local state management. The migration to or from global state should not require an entirely new skill set.
We read from and write to a local component state using the following syntax:
We should be able to harness the power of global state similarly:
Each property on the global member variable
this.global can harness a getter that subscribes that component instance to property changes in the global store. Whenever that property changes, any instance the accessed it re-renders. That way, updating property
name in the global store does not re-render a component that only accesses property
this.global.age, but it does re-render components that access
this.global.name, as would be intuitive behavior of a state change.
As a technical necessity, a global hook would need the property name (instead of a default value) in order to access that specific property. I would opt away from a default value on a global hook. Almost by definition, a global state property is to be accessed by multiple components. Having to put a default on each component, which should theoretically be the same default value for all instances of that property, is not DRY code. Global defaults should be managed externally, such as an initializer.
And if you want the entire global state object in a hook:
Though a functional component,
global would be analogous to
setGlobal would be analogous to
this.setGlobal in a class component.
No Boilerplate 🔩
Minimal Setup or Modification
When we strip a lot of the features of Redux or MobX that developers find unnecessary, tedious, or otherwise superfluous, there isn’t much boilerplate needed. Especially when we gear our package towards React itself and not on being a global state solution for the Internet as a whole.
If we want
this.setGlobal in class components, then it needs to be on the class each component extends —
React.PureComponent. That new class, with global state functionality, would extend the original
React.PureComponent. There are a few different ways to go about this. I opted for what I would consider to be the easiest for any developer: a single byte change.
The package, named reactn, exports an exact copy of React, except the Component and PureComponent properties extend the originals by adding the
global member variable and
Whenever you add this single byte to a file, all references to
React.PureComponent now have global functionality built in, while all references to other React functionality, such as
React.createElement are completely unaltered. This is accomplished by copying the references to the same React package you are already using to a new object. reactn is lightweight as a result, as opposed to a copy-paste clone of the React package, and it doesn’t modify the original React object at all.
But what if you don’t want the
React object that you import to have these new properties? I completely understand. The default import of reactn also acts as a decorator.
No decorator support in your
create-react-app? Class decorators are easy to implement in vanilla ES6.
One of these three solutions should meet the style guidelines of your team, and any of the three options have no more than one line of “boilerplate” to implement.
But what about setting up the store? The aforementioned initializer? The aforementioned redux nightmare? My best solution thus far is to simply pass a state object synchronously, but I feel it’s an area that might could use some improvement from community feedback.
React Hooks 🎣
“I’m sorry, is this 24 Oct. 2018? React Hooks are here now, and I never have to use a class component again!”
You’re right. React global state management solutions should harness the power of React Hooks — after all, functional components use
useState, so in order to be intuitive to what React developers already know and use, there should be an analogous global state hook.
We can offer a completely analogous solution; and, as it should, it shares global state with the global
text property used in the class component demo. There’s no reason why functional and class components can’t share their global states. Using hooks-within-hooks, we can force a component to re-render when a global state property to which it is “hooked” changes — just as you would expect with local state.
A little more versatile, we can use
useGlobal the same way class components use it. This may be more accessible to the user migrating from classes.
setGlobal also accepts a function parameter, the same way
Reducers: Modern Staples of State Management 🔉
With Redux’s dependency on reducers and React 16.7’s introduction of
useReducer, I simply couldn’t pretend that reducers aren’t a modern day implementation of state management. How do you manage a third party global state without the boilerplate of reducers?
I’ve implemented two solutions. One, for class syntax:
This introduces the familiarity of Redux reducers with less boilerplate: the functions are smaller and easier to code split, and there are no higher-order components to cloud the React component tree. Altogether this, to me, feels more maintainable.
The second solution is inspired by the functional
useGlobal takes a string property name as a parameter in aforementioned examples, it is unambiguous that a function parameter should be a reducer. Like
useReducer, you can use this returned reducer to modify the global state. Your reducers can thus be code split or even imported if that is preferred to the aforementioned
addReducer is preferred, you can still access your added reducers in functional components via
const addCard = useGlobal('addCard');.
This isn’t the documentation for reactn, so I won’t detail the bells and whistles. I do want to outline a system that I believe is significantly more intuitive to React developers, in hopes that it may inspire creativity that is targeted to React solutions. There is absolutely no reason a global state package should require so much boilerplate or add so much complexity to a project. All of the above takes up a whopping 8kB uncompressed, on par with redux itself, without the need for middlewares to handle asynchronous state change.
If you want to contribute to this project, it is open-source on GitHub, and I would be absolutely ecstatic for more community feedback. If you want to play around with this project, simply
npm install reactn --save or
yarn add reactn.
If you liked this article, feel free to give it a clap or two. It’s quick, it’s easy, and it’s free! If you have any questions or relevant great advice, please leave them in the comments below.