UI as an afterthought
Or: how will React context and hooks change the game of state management?
Originally published at: https://michel.codes/blogs/ui-as-an-afterthought
A question people ask me regularly: “How do all the new React features (context, hooks, suspense) affect how we build (web) apps in the future? Do they make state management libraries like Redux or MobX obsolete?”
With this post, I’ll try to answer that question once and for all! To truly understand the question, we’ll need to do a little groundwork. Let’s step back, and leave React, Redux and MobX alone while we answer a more fundamental question.
What is a web application? For the purpose of this post: A web application is a user interface that allows customers to interact with your business. The key take-away here is that it is a user interface. Not the. The goal of a nice front-end: provide a nice, friction-less experience to your customers to interact with your business processes. But the front-end is not the business itself!
As thought experiment, imagine that inbox.google.com stops working (oh, wait, it will…😭). Theoretically users could pick up the phone, call google, identify themselves and ask a google employee: please tell me, what messages do you have waiting for me? This mental exercise is a great way to figure out what your business is about. If a customer walked by your office, what questions would they ask? What pieces of information would you try to save if the office is about to burn down? What user interactions ultimately generate money of your business?
I notice that in front-end development we often approach user interfaces from the opposite angle: We start from some mock-ups and then add pieces of state at almost arbitrarily places to make the whole thing come alive. Basically, state and data is an afterthought, a necessary evil that is needed to make that beautiful UI work. Working your application from that side inevitably leads to the conclusion: State is the root of all evil. It’s that horrible thing that makes everything that was beautiful at first, ugly and complicated. But here is a counter-thought:
State is the root of all revenue.
Information. The opportunity for customers to interact with business processes are ultimately the only thing that make money. Yes, a better UI experience will most likely lead to more money. But it is not the money generator itself.
So, in my humble opinion, we should approach building web apps from the opposite direction, and first encode what interactions our customers will have with our systems. What are the processes. What is the information he will need? What is the information he will send? In other words, let’s start with modelling our problem domain.
The solutions to these problems are things we can code without reaching for a UI library. We can program the interactions in abstract terms. Unit test them. Build a deep understanding of what different states all these processes can be in.
At this point, it doesn’t matter yet what the nature of the tool is that the customers use to interact with your business. A web app? A React native application? An SDK as NPM module? A CLI? It doesn’t matter! So:
Initially, design your state, stores, processes as if you were building a CLI, not a web app.
Now you might be wondering: “Aren’t you horrible over-engineering? Why should I build my app as if I am about to release a CLI? I am clearly never going to do that…. Are you unicorn-puking me?”
Now, stop reading this blog for a moment and go back to the project you are currently procrastinating on, and fire up your test runner…. Now tell me again: Does your app have a CLI or not? Every dev on your team has a CLI (hopefully): the test runner. It interacts with and verifies your business processes. The fewer levels of indirection that your unit tests need to interact with your processes, the better. Unit tests are the second UI to your system. Or even the first if you apply TDD.
React does a really awesome job to allow unit tests to understand a component UI and interact with it (without having a browser and such). But still, you should be able to test without the indirections introduced by concepts such “mounting”, “rendering” (shallow or not?), “firing events”, “snapshotting UI”. These are all concepts that don’t matter for the business domain, and unnecessarily bind your logic to React.
Nothing beats the simplicity of invoking your business processes directly as a set of functions.
So, at this point you might have some clue why I always have been an opponent of directly capturing domain state in React component state. It makes the decoupling of business processes and UI unnecessarily complicated.
If I was to build a CLI for my app, I would probably use something like yargs or commander. But that doesn’t mean that because a CLI happens to be my UI, that I expect those libraries to suddenly manage the state of my business processes. In other words, that I would be willing to pay for a full rewrite, just to switch between yargs and commander. React is to me like a CLI lib, a tool that helps to capture user input, fire of processes, and to transform business data into a nice output. It’s a library to build user interfaces. Not business processes.
Only when you have captured the customer processes, tested, and verified them, it starts to matter what the actual UI should look like. What technology it is built with. You will find yourself in a very comfortable position: As you begin building components, you will find that they don’t need that much state. Some components will have some state of their own, as not all UI state is relevant for your business processes (all volatile state like current selection, tabs, routing, etc). But,
most components will be dumb.
You will also discover that testing becomes simpler; you will write way less tests that mount components, fire events etc. You still want some, to verify that you wired everything correctly, but there is no need to test all possible combinations.
The great decoupling allows very rapid UI iteration, A/B testing, etc. Because once the domain state and UI have been decoupled, you are much more free to restructure your UI. Heck, even switch to a completely different UI lib or paradigm becomes cheaper. Because state is largely unaffected by it. Which is great, as in most applications I’ve seen the UI develops in much higher pace than the actual business logic.
For example, at Mendix we used the above model with great success. This separation has become the paradigm everyone naturally follows. An example: The user needs to upload an excel sheet, next we run some client side validations on it, then we interact with the server, and finally we kick off some processes. Such a new feature would first result in a new store (just a plain JS class) that captures the internal state and methods for every step of the process. It would capture the logic for verification. The interactions with the back-end. And we would create unit tests to verify that the right validations messages are generated and that the whole the process works correctly under all it’s state permutations and error conditions. Only after that point, people start building the UI. Pick a nice upload component, build forms for all the steps. And maybe change the original validation messages if they don’t sound right.
At this point you might also understand why I am not a fan of the things that mix back-end interaction directly into the UI. Such as the react-apollo bindings as means to interact with graphQL. Back-end interaction like submitting mutations or fetching data is the responsibility of my domain stores. Not the UI layer. React-Apollo so far feels to me as a shortcut that too easily leads to a tightly coupled setup.
Finally! It is time to go back to our original question: “How do all the new react features (context, hooks, suspense) affect how we build (web) apps in the future? Do they make state management libraries like Redux or MobX obsolete?”.
The answer for me is: The new features don’t change the game of state management. The context and hooks features don’t enable React to do new tricks. These are just the same tricks, significantly better organized, more composable and in a less error prone way (clearly, I’m a fan!). But React, out of the box, can only respond to state that is owned by components. If you want your domain state to live outside your component tree, you will need a separate state management pattern, abstraction, architecture, library, to organize that.
Recent React additions don’t fundamentally change anything in the state management game.
In other words: if you just figured you don’t need Redux or MobX anymore since the introduction of Context and hooks: Then you didn’t need those before either.
Note that with hooks, there is now less reason to use MobX to manage your local component state. Especially considering that MobX observables as component state won’t be able to leverage the benefits of suspense.
Talking about suspense versus state management in general: I think it just proves the sanity of the separation of concerns: Suspense + React state is great to manage all the UI state, so that there can be concurrent rendering and such. Supporting concurrency makes a lot of sense for volatile state like UI state. But for my business processes? Business processes should be exactly in one state only at all times.
So, with that, we hopefully answered the question about modern React versus state management:
React should manage volatile UI state, not your business processes. And for that reason, nothing really changed.
- Be able to manage state independently of any UI abstraction.
- Neatly and transparently hook up the state they mange to the UI.
- Avoid the error prone business of managing subscriptions, selectors, and other manual optimizations to make sure events don’t cause to many components to re-render.
If you want to know how cool working with separately organized domain state is, watch my “Complexity: Divide & Conquer” talk or read this previous blog: “How to decouple State and UI”. Olufemi Adeojo wrote about this recently as well: “The curious case of reusable state management”.
Before we part, let’s go meta: every blogger knows that a blog needs images to engage users. This blog didn’t feature any images yet and hence has a poor, sub-optimal UI. But still can perform all it’s “business goals”: Sharing the above thoughts with you. Because, although extremely crucial, from an implementation perspective:
UI is just an afterthought.
Tip: Using MobX with React 16.8.0 or higher? Check out the hook based MobX-react bindings which are much smaller than the original ones!
Michel Weststrate is OSS maintainer, author of MobX, Mobx-state-tree, Immer and independent trainer / consultant at michel.codes