Recently, I did a side project that I wrote about in the other post. As part of it, I had what was supposed to be just a few paragraphs on how React sucks - but I just couldn't stop writing about it.
So here it is a full, standalone blog post, even bigger than the one it sprang from, all about how React sucks. And how it might not even be its own fault.
In my junior days, down on the streets, I used to do Angular.JS for money. At the time, it was a seriously good piece of technology. Definitely the biggest JS framework of its time, and what's most important to its legacy, probably the first time web development has had a "framework". Prior to that, they were all "libraries", so this was the first one that gave not only you a set of functions to use, but the actual framework in which you built your web app.
But things are always only good in relative, and Angular was good because it's predecessors were not. At the time, we had other SPA frameworks like Backbone and Knockout, but they didn't leave as much of an impact. No, the real enemy that Angular had beaten was jQuery.
Even though jQuery was only a wrapper over (at the time admittedly very shoddy) HTML DOM APIs, it still became a de-facto standard if you wanted to build complex web applications. How it worked was pretty straightforward: you create HTML elements in JS, manually and imperatively, then modify them, move them around, do whatever it takes to make the website interactive like it's an app.
This all works completely fine for simple apps, but you can imagine it becoming a maintenance nightmare in case of anything bigger. And that is exactly what started happening around. You can't really blame jQuery, but only the appetites of modern users who needed that kind of interactivity everywhere. So developers were blindsided to keep using jQuery even though it was not a good fit for the job anymore.
Then Angular arrived and had it all sorted out. You could focus your energy on writing the UI and app logic instead of manually assembling individual pieces of HTML. It truly was a game-changing library framework, as you finally had a proper tool to make BIG interactive applications. Some magic things it had:
A) Components. Ok, it had a weird naming so these were actually called "directives", but in any case you could define a simple HTML and JS file combo representing a piece of UI and then reuse it across multiple places in the app.
B) Two-way binding. You define a variable, and whenever it changes, all the places in the UI are updated. This worked really well. Later, people started nagging that this omnidirectional data flow is bad, so there was a push to use one-way (top-bottom) bindings instead, which does sound technically better, but in practice made everything more complicated and started a strand of discussion which ended with us all having to use Redux today. So thanks.
On my first job I worked on exactly one such rewrite of an huge, unwieldy jQuery app into an Angular app. Both the process and the end results were pretty good.
What was not good, though, was having to rewrite the exact same screens in Angular 2 a few years later, and I'm just happy I left that company early enough before they made me rewrite it for the third time in React later.
I did get a chance later to get to learn React and even use it professionally on a project or two.
I still remember seeing how fresh it looked on first glance. At the time, the contrast was with the framework of the day, which was Angular 2 - a complete rewrite of the original, but now with twice the boilerplate, Typescript-out-of-the-box, one-way binding, reactive/observable patterns - all good things on their own, but god damn was it complicated, slow to work on, slow to build, slow to run.
React swung the pendulum back to the simplicity and people were up all for it. And for a while, simplicity remained, React gained popularity to become the #1 library for making SPAs.
Yes, now we were using the term "library" again, showing how simpler it really was. But you can't reasonably build a complex app with just a library. You need few of them to handle all of the app's concerns, and you also need some code structure. React's "bring your own beer" approach meant you basically built a framework yourself, with all the downsides it had.
The end result - no two React apps were the same. Each of them had a bespoke "framework" built out of random libraries found on the internet.
The apps I had the misfortune to work on at the time all made me think the same thing - even Angular 2 would be better than this. The JSX "core" always seemed solid, but everything around it was just plain mess.
So I got out and went writing some Java backends, which I believe says it all.
They say a man can never really learn anything - you either know something or you don't. I apparently don't, so I dragged myself back into React recently.
Granted, it was a hobby project, so I didn't experience it "in full" like I would if it was a serious production app. But still, even this experience both confirmed and greatly exceeded my low expectations for it. React feels insane and I don't know how no one else is talking about it.
First, let's start with the architecture React enforces for you. As said before, React is only a library, so it's not forcing you on anything, but still, the implicit constraints of having JSX make some patterns surface on their own. Eons ago, we used to talk about MVC, MVVM, MVP, all of which only a variations on the same theme, so which one is React? None, I believe this is a new-ish paradigm - I think we could literally call it "components-based architecture".
On first glance, it's all logical. You have components, you build a top-down tree of them, and bam there's your app. React does some internal magic to make sure it's up to date with the data you give it. Simple enough.
But sometime along the way, it all started acting smarter than it should really be. For a simple "UI library", React sure has a lot of loaded terminology. And for a library that doesn't have anything to do with "functional programming", it sure has a lot of functional programming names inside.
Let's start from the state. If you have a top-down tree of components, it's logical you'd want to pass the state top-down too. But in practice, with components very numerous and small, this is very messy, as you spend a lot of time and code just wiring the various pieces of data to get them where you need them.
This was solved by "sideloading" state into components using React hooks. I haven't heard anyone complain about this, but are you guys serious? You're saying that any component can use any piece of app state? And even worse, any component can emit a state change, that can then update in any other component.
How did this ever pass a code review? You are basically using a global variable, just with more elaborate state mutation rules. They're not even rules, but merely a ceremony, because nothing is really preventing you from mutating state from anywhere. People really think if you give something a smart name like a reducer it suddenly becomes Good Architectureâ„¢?
So if both top-down and sideloading approaches suck, what would be the solution for this? I honestly don't know. In fact, the only thing I can think of is: if we can't solve this nicely, then maybe the entire "components architecture" was a mistake and we shouldn've called it a paragon of Nice Design and stopped innovating. Maybe this time for a change we really did need yet another JS framework that would try something better.
Next on in the "things we're unsure how they passed a code review", let's riff on React Hooks. There's no denying they're useful, but their existence even to this day raises question marks above my head.
I won't even mention how people talk about components as "pure functions" but then have hooks as a tiny stateful black boxes inside of them. And given their composable nature, it's more like layers and layers of tiny stateful black boxes.
But no, I'd mostly like to roast useEffect here. It's simple what a "side effect" would be. You change a state and then you need to do some external action, like post the results to an API. This split between the "important app stuff" and "side effects" makes sense - in theory. But in practice, can you ever split it cleanly like that?
My biggest gripe, for starters, is that useEffect is used as a "run something after the component mounts". I understand when React migrated from classes to hooks, this was the closest alternative to componentDidMount, but come on - how is this not considered a huge hack?
You're using a "side effect" hook to initialize the component? Ok, if you have to make an API call from there, I'd agree that would be a side effect. But then that API call... it... it sets the state too. So a completely innocous "side effect" hook actually manages a state of the component. Why is no one talking about how crazy this is?
Moreover, if you wanted to depend on that state and do something after it, then you... you... define yet another useEffect with a dependency on what the first one sets.
This is a code that I have taken from a production app of company recently acquired for several tens of millions of US dollars. I slightly redacted it here to use a simpler House and Cat entities instead of what is actually there. But go take a look and try to parse in which order is this code executed. When you're ready, the answer is in an image below:
So something like that, a series of state mutations that would otherwise be a simple imperative code is now... spread out across two asynchronous functions, where the only hint of the order of their execution is the "dependency array" at the bottom of each. And the way that you actually mentally parse it is, in fact, from the bottom to the top.
I remember how Javascript promises were considered unwieldy with their thens, and even before them we had "callback hell" - but literally anything would be better than this.
I understand these issues could be solved a) by moving them into a separate file, which is just hiding the problem, or b) probably with some Redux or something, but I really don't have enough mileage with it to know for sure.
All of this combined looks ugly, and betrays the simplicity that React promised in its "Hello world" example. But wait, I'm not done yet. I read a blog post from an acquaintance called "The Most Common React Design Patterns". Expecting I don't know what, I was still shocked at how complicated these are and how much mental overhead there is to simply figure out what is happening - and all of that just to render a list of items on a screen.
The most jarring thing: the article doesn't even acknowledge it. All this complexity is taken from granted. People apparently really build their UIs like this and no one bats an eye.
Then, as if that isn't enough, some of you go as far as to write "CSS-in-JS", and then get paid for it. I agree that JSX initially showed that "separation of concerns" is not "separation of files" and that it's actually okay to write your HTML and JS in a same file. But chucking in CSS in there too and making it strongly typed? Isn't this a step too far?
It would be too easy to just say React is, well, downright insane, and go on with our lives. But as reasonable primates, I believe we can do better. We can try to understand it.
I am once again trippin' down the memory superhighway to get reminded of my first job and a colleague from the mentioned "jQuery migration" project. A super-experienced backend engineer, an architect type, and overall a very respected guy when it came to all things software.
What I remember the most about him are not his technical solutions, but the amount of judgement he'd have shown on anything we did on frontend. Looking anything on the Angular app, he was like - what the hell are you guys doing here? Why does this have to be so complicated?
And it's not that we sucked - we too were a no-nonsense crew about software. It's just that at the time, through the eyes of a classical backend developer, the entire Angular setup seemed absolutely insane.
Today, I'm roughly the same age as he was then, and I am here writing a blog post about how Angular React is insane. Some things are inevitable, I guess.
But let's rise a step above and try to understand why it could be so.
Firstly, I think we can all agree that most web apps shouldn't even be web apps in the first place. People go the way of SPA even if they don't need a SPA right away, but they might need it later, so it apparently doesn't cost as much to go with SPA from scratch.
But I'd argue here that such a move, in fact, does cost you. It's just that we're so entrenched in the "SPA-by-default" way that we forgot how simpler the alternatives are. Having a simple dumb server-side-rendered page is orders of magnitude simpler than even thinking about React. There's no overhead with API communication, frontend is very lightweight, your UI code can be strongly typed (if your backend is strongly typed), you can do refactors across the full stack, everything will load faster, you can cache it better because some components are very static and remain the same for all the users so you can render them only once, etc, etc.
You do lose the flexibility to have complex interactive logic at your product manager's whim, though. But that's maybe only partially true, because I'd wager you could go a pretty long way with just plain Javascript "progressive enhancement" before you really get to a state management complex enough to warrant adding React in there.
Ok, so I'm saying we use React simply because we've used it before. No wonder, inertia is a hell of a drug, but it still doesn't explain why this code ends up being so unthinkably complex.
My answer to that question, surprisingly, stops roasting React and goes the opposite way, defending not only React, but also Angular and jQuery and everything that came before them. I think this code is bad because making a interactive UI where any component can update any other component is simply one of the most complicated things you could do in software.
Think of any other system you use in your everyday life. Your kitchen sink has two inputs, hot and cold, and one output, a water running. Your kitchen mixer or a power drill might have a button or two, and still whatever you do, it only affects the action on the spinning part. An oven might have three or four or five knobs and maybe the same number of outputs, and already that is starting to sound pretty dangerous.
In contrast, an interactive UI that we have on web can have potentially infinite number of inputs, and potentially infinite number of outputs. How could you even expect to have a "clean code" for this?
So, this entire rant about React... it's not even React's fault. Neither is Angular's, or jQuery's. Simply, whichever tech you choose will inevitably crumble down under the impossible complexity of building a reactive UI.
How could we fix this? I'm not smart or in-the-weeds enough to really solve this problem, but I can spitball some ideas. If we adopt this input/output mental model of a webpage as a real thing, then maybe we could start working on reducing a number of its inputs and outputs.
On the inputs side, yeah, this is me saying: "go have fewer buttons", which may not always, or ever, be enforceable. But certainly, the less features you have, the more manageable your codebase is.
It's straightforward enough to need no mentioning - or is it? Do product managers know that adding three buttons instead of two will cause 5% more bugs and make any future work on that screen 30% more complicated to design and implement? No one is even measuring those things, but I believe they could be true.
Why is it that, if I told you we need to add Redis on backend, you will tell me "no, we need to curb the technical complexity" - but if a product manager asks to add a global app-wide filter that could be applied from anywhere and to anything, you'd just get your head down and write some monstruosity that people will spend the next 10 years trying to get out.
In short - please, stop adding so many buttons, I beg you. You could even, I know, crazy, try to remove some of them?
On the outputs side, however, the story is a bit different. Writing this makes me realize that having a server-side rendered page is basically reducing the page to a single output. Anything you interact with, it just rebuilds the entire page. This means that, ironically, removing FP-inspired React from the mix makes a server-side rendered page an actually a pure function of the state. No frontend state = big simplicity wins, if you could afford it.
Inevitably, when you do need some scripting logic in your server-side rendered "app", maybe the smart move would be to add it only on the most necessary places. The smallest you could go with, the better.
I thought a good name for this would be "islands of interactivity". Then I Googled it and turns out that's already a thing. Although, that post still mentions Preact, SSR, manifest files, so I'm not sure we're really on a same page. People will overcomplicate everything.
But I do believe we have enough bandwidth today that you can load a small React app that only renders an island of interactivity inside of what is a classic server-side rendered page. I don't believe that mix would be that abominable, but I've yet to try it, and for my next project, I just might.
So, my untested approach to having clean and maintanable frontend code is: go render it all on server and plop in React or whatever only where you really need it.
It really can't be any worse than this.
-------
(Side note, I'm trying something new this time (no, it's not Patreon) - here are the "official" comment threads for this blog post on HackerNews and on Reddit. To keep up-to-date, you can also subscribe to my newsletter.)