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.
You may use this implementation yourself via reactn
on NPM or contribute to, fork, or otherwise spy on the open-source GitHub repository.
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.state
, this.setState
, and the new useState
and useReducer
hooks. 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:
// Class Component
this.state.name;
this.setState({
name: 'Charles',
});
// Functional Component
const [ name, setName ] = useState('Default Name');
We should be able to harness the power of global state similarly:
// Class Component
this.global.name;
this.setGlobal({
name: 'Charles',
});
// Functional Component
const [ name, setName ] = useGlobal('name');
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:
const [ global, setGlobal ] = useGlobal();
Though a functional component, global
would be analogous to this.global
and 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.global
and this.setGlobal
in class components, then it needs to be on the class each component extends — React.Component
and React.PureComponent
. That new class, with global state functionality, would extend the original React.Component
or 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 setGlobal
method.
import React from 'react'; // before
import React from 'reactn'; // after
Whenever you add this single byte to a file, all references to React.Component
and 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.
import React from 'react';
import reactn from 'reactn';
@reactn
export default class MyComponent extends React.Component {
render() {
return <div>{this.global.text}</div>;
}
}
No decorator support in your create-react-app
? Class decorators are easy to implement in vanilla ES6.
import React from 'react';
import reactn from 'reactn';
class MyComponent extends React.Component {
render() {
return <div>{this.global.text}</div>;
}
}
export default reactn(MyComponent);
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 could use some improvement from community feedback.
import { setGlobal } from 'reactn';
setGlobal({
a: true,
b: false,
name: 'Charles',
age: 'Forever 21'
});
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.
import React, { useState } from 'react';
import { useGlobal } from 'reactn';
const MyComponent = () => {
const [ localText, setLocalText ] = useState('Hello world!');
const [ globalText, setGlobalText ] = useGlobal('text');
return <div>{localText}... {globalText}</div>;
};
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.
import React from 'react';
import { useGlobal } from 'reactn';
const MyComponent = () => {
const [ global, setGlobal ] = useGlobal();
return (
<button
onClick={() => {
setGlobal({
x: global.x + 1
});
}}
>
Click Me {global.x}
</button>
);
};
setGlobal
also accepts a function parameter, the same way this.setState
does.
setGlobal(oldGlobal => ({
x: oldGlobal.x + 1
}));
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:
import { addReducer } from 'reactn';
// this.dispatch.addCard('Ace of Spades')
addReducer('addCard', (state, dispatch, card) => ({
cards: state.cards.concat([ card ]),
}));
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 useReducer
.
import { useDispatch } from 'reactn';
const addCardReducer = (cards, card) =>
cards.concat([ card ]);
const MyComponent = () => {
// addCard takes a card and concats it
// to the global state cards property.
const addCard = useDispatch(
addCardReducer, // <-- reducer function
'cards', // <-- the property being reduced
);
// Display a button.
return (
<button
onClick={() => {
// Add "Ace of Spades" to the global state.
addCard('Ace of Spades');
}}
>
Click me
</button>
);
};
Like useReducer
, you can use this returned dispatch function to modify the global state. Your reducers can thus be code split or even imported if that is preferred to the aforementioned addReducer
. If addReducer
is preferred, you can still access your added reducers in functional components via const addCard = useDispatch('addCard');
.
Conclusion 🔚
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 4.3kB, and supports asynchronous state change out of the box (with no need for middleware).
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
or yarn add reactn
.
If you liked this article, feel free to give it a heart or unicorn. 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.
To read more of my columns, you may follow me on LinkedIn, Medium, and Twitter, or check out my portfolio on CharlesStover.com.
Top comments (1)
Do you ever find it difficult with hooks or reactn global to discover the global state? Or does it ever get too tied to a component?