Mistakes were made
Using application state, or global state, is not the right answer for developing front-end components.
The draw of application state is the creation of a definitive single-source of truth from which our entire app’s state can be determined. Then, this definitive state becomes the one and only place where you need to query state. And because it is ubiquitous, adding a new shared property becomes extremely simple; add it once, and it’s available everywhere.
This ease of modification is, unfortunately, double-edged. I’ve noticed two messy patterns arise once global state is available.
There is no local; everything is global
The lure of global state is such that once it is implemented, it has a tendency to supplant local state management. Simple UI changes that could have been implemented as local state changes creep into global state because their values could be used to affect other components in cool ways.
Here’s a simple example of this: On Turaku (my late pet password manager project), editing the title of any entry using the editor component changes the title of the entry in the entries list component immediately. This was extremely easy to implement because the two components shared the data about entries via the application state.
The drawback is that even though the responsibility for changing an entry lies logically with the editor component, something as simple as saving changes becomes a sticky proposition. Questions arise:
- What happens if the user clicks on another entry in the list when one has unsaved changes? Sure, the unsaved changes would persist - they’re in app state, but the old editor would be unmounted eventually…
- So which part of the UI should the user interact with to initiate the save?
- Maybe we should auto-save if the editor un-mounts as a result of user switching to another view..? But which part of the UI would show that there is a network operation on-going? What happens if the network operation fails?
The last option is what I went with, and I never answered all of the questions fully. I just slapped a few chunks of code together to get everything working, and promised myself that I would get around to cleaning it up eventually.
This was me ignoring the KISS principle.
The simple solution here would have been to isolate the edited state of an entry to the component that does the editing - the editor, and communicate changes to higher-level components only once those changes were persisted. A simple UI curtain could have been employed to ensure that users couldn’t interact with unrelated parts of the UI while the edit was in progress.
What should have been simple - making a few edits to an entry, and then saving it ended up involving many different components, and is now unnecessarily complicated because I couldn’t say “No” to a few cool things.
Application state is god; all responsibilities are centralised
I think that SRP is more of a guideline than a rule when it comes to writing applications, but by its very nature, application state seems require that SRP be broken.
When we collect state from different parts of the application, it’s almost impossible for all of these values to be (closely) related. Usually, they’re collected over time as the application’s feature-set expands and evolves, and as such they’re going to be incorporated into a single reducer that, increasingly needs to handle values with different purposes.
The issue that pops up as a result is increased cognitive load. It’s easy to make changes when the number of things that can change is low. The larger the size of the state being managed by a reducer, the harder it is to simply fit all of its variables into our working memory.
When asked for his opinion, here’s what Jasim had to say about this topic:
One solution to large reducers that we’ve been using more and more is to lean on domain-specific modules, and use the reducer just as a very high-level dispatcher.
UI handler functions in lower-level components can run their own computation and then pass final results back to a reducer. This keeps the responsibility of each domain closer to the component that handles it, and the implementations are all co-located neatly in their own modules.
Back to Basics
When reaching for global / application state as a solution from our toolbox, its critical to examine whether it’s worth the costs involved, and whether we’re being careful to avoid the pitfalls that it exposes.
State-management based on localized state with plentiful prop drilling might not be stylish, but it’s based on sound development practises, works really well in most situations, and should be the default choice for React and ReasonReact applications of any level of complexity.
As for my pet project, it’s future is uncertain. I started working on it when I was still a beginner with ReasonML, and the mistakes I made while learning have piled up. Its state management is, quite frankly, a mess. However, thanks to those mistakes, by the time I introduced ReasonML & ReasonReact to my full-time work, I knew enough to be conservative.
Right now, I’m focusing on building PupilFirst - an LMS that my company open-sourced recently, and which we’ve been working on since 2013. It mixes Ruby, ReasonML & GraphQL, and is probably one of the largest open-source ReasonReact webapps out there.