This is one of those strange posts I never thought I’d write.
For the record, I love ReactJS. The principle of being able to build an entire website using forward-only data flow was sheer raw brilliance, and was a fundamental architectural shift in how website UIs work.
But —
No, seriously, I really do love ReactJS. It’s conceptually amazing, a genius abstraction that transforms the old shared-state UI-update model into something closer to the video-game model where you compute the next application state, and then just re-render everything, and somehow the UI is magically fast enough to handle it. Everything about the way UIs are looked at is different now: Whether it’s a web page or a more traditional UI system, now you want to think about it as simple, clean forward-only data flow and events that bubble back up, not as a tangled spaghetti blob of tiny stateful components. So many of the classic state bugs are impossible in the React mindset that it’s immediately obvious that it’s a better way of thinking.
But —
How did we get here. How did we get here…
-shakes head-
I’ve written and debugged a lot of code over the years. Millions of lines, on machines big and small. I’ve debugged, fixed, optimized, refactored and cleaned up code I wrote, code other people wrote, code the universe wrote, and I’ve seen it all: Every pattern, big and small, good and bad, from a forgotten comma all the way up to major architectural data-flow and storage-model disasters.
And through it all, two laws have always won out:
Business code is boring, so programmers will invent “clever” solutions to entertain themselves.
— Ben Przystanski
All non-trivial abstractions, to some degree, are leaky.
— Joel Spolsky
Those are killer rules, but they’ve rarely let me down.
That first rule says that people don’t implement “Hello World.” They implement a dependency-injected AbstractMessageProvider<T>. They don’t implement a “Submit” button. They implement sagas of Redux reducers initiating dataflows through abstract streams.
And that second rule says they’re going to trip on their own shoelaces.
That AbstractMessageProvider<T> is going to be nice and flexible for a ton of use cases it’ll never be put to, and it’s going to make the program throw an exception instead of printing when the output is piped to “/dev/null”. That big pile of Redux reducers implementing that multi-server saga is going to work really well until somebody presses the browser’s Back button, and suddenly the application is stuck in an undefined state.
Building good abstractions that save programmers time and energy is hard. It’s not for the faint of heart, or the weak-minded. Odds are around 90% that you’ll get it wrong if you try. You have a nine-in-ten chance of building something that’s going to be barely usable for your current circumstance, much less for future use cases, and much, much less for the poor future soul that is going to have to come after you and debug and maintain your brilliant shining gemstone of a design that turned out in implementation to actually be a bit closer to a hunk of pumice with some of the sharpest edges rounded off.
That’s not to say we shouldn’t be trying to build smart, clever solutions to save programmers time and energy. That’s to say you shouldn’t be. You don’t really know what you’re doing, and your smart, clever solution is going to be a mess and cause pain. (This statement is true for all values of “you.”) If you’re lucky and really, really talented, your abstraction won’t leak too often, and your abstraction won’t result in programmers writing code with your abstraction that is substantially more complicated than the code would’ve been without your abstraction.
And that brings us back to ReactJS.
ReactJS is amazing. Really. The basic principle of forward-only dataflow is so alluring and beautiful that the entire intarwebz has jumped onto it, and rightly so.
But after watching it in use for the better part of the last five years, I think we don’t know how to handle it. We used to write —
$(".fooButton").click(function() {
$.ajax("/update-foos", { method: "POST" });
});
which was prone to all sorts of stateful shenanigans, but which worked. In two or three lines of code, it solved a business problem, and it did so in a small enough space that when a bug was found, mere mortals could fix it. — and they’d probably introduce another bug in the process, but at least you could work with the code.
Now, instead of that, we write —
import React from 'react';
import { axios } from 'axios';
import { store } from './stores/redux-store';
import registeredActions from './actions';
import ...
// Actually, I'll skip the rest of the imports or we'll be here all day.
function fooReducer(state = {}, action) {
switch (action) {
case [registeredActions.FOO_ACTION.toString()]:
axios.post(serviceUrls.primaryUrls.fooService.updateFoos, {})
.then(() => store.dispatch(notifyFooServiceComplete()));
break;
default:
return state;
}
}
const fooButton = mapStateToProps(({ props }) =>
withStyles(className =>
(<button className={className} onclick={() => store.dispatch(fooAction())} {...props} />))
);
export default fooButton;
Oh, wait, I forgot the REST backend has been superseded by a GraphQL service-aggregating endpoint now.
And the team decided we’re going to wrap everything in TypeScript to ensure we don’t accidentally pass fooState into the barReducer, so that code up there is going to need a lot more angle brackets in it.
And now we have a bug somewhere in the pipeline after a button click where the store seems to have bad data in it but we can’t seem to figure out which event is triggering it and none of the breakpoints in the reducers are firing and every attempt to step into the code lands us inside a function named ‘d’ with parameters ‘q’ and ‘x’ because it only seems to fail when the code is minified on the production site, and after hours of debugging we’ve nailed it down to a Babel plugin that adds support for an experimental JavaScript feature that’s been secretly injecting extra parameters into half our functions.
How is this better?
We write hundreds and hundreds or even thousands of lines of code to solve simple problems, pretending to ourselves that they’re making the code safer and more stable and easier to adjust and refactor, but all we’re doing is pretending. React itself is beautiful, but the entire ecosystem surrounding it is a disaster for comprehension and debugging and stability and maintenance. Each of those abstractions leak 5% of the time, and instead of the result being leaky only 5% of the time, the result leaks like the product of all the individual abstractions it’s made of, and 50% of the time this brand-new shiny software is buggy, glitchy, unmaintainable, is failing in QA, and is making our customers yell at us.
We — all of us — are not mature enough yet for abstractions like React.
And ReactJS isn’t the only place where this happens. It’s just the flavor this week. Whether your morass is a pile of DDD generic type abstractions on top of Entity Framework in C#, that ProxyProxyStrategyFactoryProvider in Java, 147 interlocking Rust macros that are guaranteed to produce a perfect state machine if only they’d compile, or insisting for the umpteenth time that this inscrutable stack of Haskell monads is so perfect if only anyone would really pay attention — we do this everywhere. We trade simple computation for hundreds of “elegant abstractions,” and by the time we’re at the bottom of the pile, ready to implement some actual logic to solve a problem, we don’t even remember what problem we’re trying to solve anymore. And Lord help anyone who wants to debug it after we’ve built it.
So as much as I love ReactJS, I think I’m going to set it aside for a lot of the projects I work on. I’ll use it if I have to, but I wisely won’t start new projects in it. No-one in this industry is ready to hold a light saber yet, and we’re all just getting burned. When we’ve all matured a few more decades, the light sabers will still be there, waiting for us to pick them up. But in the interim, we should make an effort to stick to wooden swords, because dull weapons might not be able to slice through trees, but they can’t cut your foot off either.