React Still Reeks of Madness, but Everyone Is Silent About It
A veteran developer's critical look at React's evolution from simple library to complex ecosystem, arguing that component-based architecture, hooks, and useEffect create unnecessary complexity that nobody dares to question.
The Ancient Angular
When I was still a junior and just learning the profession, I got to work with Angular, the popular "enterprise" JS framework. At the time, it was a pretty good technology. Clearly the biggest JavaScript framework of that era. Plus, you could credit it with being arguably the first framework for web development. Before that, there were only "libraries," so Angular not only gave us a set of functions first but also became a real framework on which you could build a web application.
But everything is relative, and Angular was only good because it significantly surpassed previous solutions. At the time, there were other frameworks for building single-page applications like Backbone and Knockout, but their mark on history was less significant. The real competitor that Angular defeated was jQuery.
Despite jQuery being just a wrapper for the (then quite terrible) HTML DOM API, it still became the reference solution for building complex web applications. Its working principle was fairly simple — you manually and imperatively create HTML elements in JS, then modify them, move them where needed, and do everything necessary to make the site work interactively as if it were an application.
And all of this works beautifully for simple applications, but you can imagine the maintenance nightmare that arises when building large programs. And you shouldn't blame jQuery, but rather the appetites of modern users who demanded this interactivity everywhere. In the end, developers were forced to keep using jQuery despite it no longer being a good choice for current tasks.
Then Angular arrived and sorted everything out. Now you could direct your efforts toward writing UI and application logic instead of manually assembling individual bits of HTML. This library — er, framework — was truly revolutionary, because we finally had a tool for creating REALLY BIG interactive applications. Here are just some of its magical properties:
A) Components. To be precise, they were called "directives" because it used a strange naming system. But regardless, you could write a simple file using HTML and JS representing a UI element and use it in different places throughout the application.
B) Two-way binding. The idea was that after defining a variable, when you changed it later, all connected areas in the UI would update. Later, people started complaining about this omnidirectional data flow, considering it a bad idea. Then a trend toward one-way bindings (top-down) emerged, which technically sounds like a better solution but in practice only complicated everything and led to a discussion that ended with us using Redux today. So, thanks for that.
At my first job, I witnessed these trends firsthand when I was rewriting one of those clunky jQuery applications into Angular. Both the process and the end result made me happy.
But there were unpleasant moments too. For instance, I didn't enjoy rewriting those same screens in Angular 2 a few years later, and I'm glad I managed to leave that company before they could make me rewrite everything a third time — this time in React.
Getting to Know React
Later I got the opportunity to learn React and even use it professionally on a couple of projects.
I still remember how fresh it looked at first glance. Back then, React stood in stark contrast to the then-current Angular 2 framework, which required completely rewriting code from its predecessor but now with double the boilerplate, one-way binding, TypeScript, and reactive/observable patterns. All these features are wonderful by themselves, but damn, they complicated everything so much, slowed down work, builds, and code execution.
React swung the pendulum back toward simplicity, and people appreciated it. For a while, simplicity really was maintained. React gained popularity and became the #1 library for building single-page applications.
Yes, now we were using the term "library" again, demonstrating this tool's simplicity. But you can't adequately create a complex application with just one library — you'll need several to handle all its tasks, plus a full code structure. React's "bring your own drinks" approach meant you build the framework yourself, with all the associated drawbacks.
Ultimately, every React application turned out to be unique. Each one created a "framework" consisting of random libraries gathered from across the web.
All the applications I had the misfortune of working with during that period led me to one conclusion — even Angular 2 would have been better for this. The JSX "core" was always solid, but everything surrounding it was complete chaos.
In the end, I gave up this lost cause and went to write backend in Java. I think my choice speaks for itself.
Just When I Thought I Was Out...
They say learning isn't the same as understanding. Apparently, I never fully understood, so I recently dove back into the depths of React.
Good thing it was a hobby project, so I got a less "full-fledged" experience than I would have with a serious commercial application. But even this modest experience was enough to confirm and amplify my negative expectations of this tool. Working with React is some kind of madness, and I don't understand why nobody talks about it.
Architecture, Components, State
Let's start with the architecture that React forces you to use. As I already said, React is just a library, so it doesn't obligate you to do anything. But still, the implicit constraints tied to JSX reveal certain patterns. Long ago, we talked about MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter) — all of which are just variations on a theme. And which one does React imply? None. I think it's built on the newest paradigm, which could literally be called "component-based architecture."
At first glance, it all makes sense. You have components, you build a top-down tree from those components, and you get your application. Meanwhile, React internally does its magic, keeping all components up to date based on the data you provide. Nothing complicated.
But somewhere in its evolution, this library started getting clever. For a simple "UI library," there's clearly way too much complex terminology. And for a library that has nothing to do with "functional programming," React has an awful lot of names from that domain.
Let's start with state. If you're building a top-down component tree, you'll probably want to pass state top-down as well. But in practice, given the huge number of small components, this creates confusion because you spend tons of time and code just wiring data elements, directing them where they need to go.
This problem was solved by "sideloading" state into components using hooks. I haven't heard anyone complain about this solution, but I'm begging you, are you serious? You're saying any component can use any piece of the application's state? Worse, any component can trigger a state change that can affect the state of any other component.
How did this solution even pass code review? You're essentially using a global variable, just with fancier rules for state mutation. And they're not even rules — it's just ceremonial convention, since nothing prevents you from changing state from any part of the program. Do people really think that if you call something a fancy name like "reducer," it suddenly becomes Good Architecture?
But if both the top-down approach and "sideloading" are bad solutions, then what would be the answer? Honestly, I don't know. One thought comes to mind: "If we can't solve this elegantly, maybe the entire 'component-based architecture' was a mistake, and we shouldn't have crowned it as the exemplar of 'good design,' stopping our search for better solutions. Maybe this time we really do need another JS framework that tries better approaches."
React Hooks
Next on the list of solutions that somehow passed code review: React hooks. Nobody argues they're useful, but their existence to this day raises questions for me.
I won't even get into the fact that people call components "pure functions" but then stuff little black boxes of state inside them. And given their compositional nature, it's more like layering tiny black boxes on top of each other.
What I want to focus on specifically is useEffect. The "side effect" it implements is simple and clear. You change state, and then you need to perform some external action, like sending results to an API. This separation between the "important part of the application" and "side effects" makes sense — in theory. But can you also cleanly separate them in practice?
What bothers me most about useEffect is that this hook is used for the task of "doing something after the component mounts." I understand that after React's migration from classes to hooks, this was the closest alternative to componentDidMount, but tell me — isn't this a massive hack?
You're using a hook that implements "side effects" to initialize a component? Fine, if you need to make an API call from a hook, I agree, that would be a side effect. But the API call... it also sets state. In the end, this absolutely innocent side-effect hook actually manages the component's state. Why is nobody pointing out this madness?
Moreover, if you wanted to create a dependency on that state and do something after it, you would... define yet another useEffect that depends on the result of the first one.
I took this code from a production application at a company that was recently acquired for several tens of millions of US dollars. I edited it slightly, replacing real entities with simplified House and Cat. But just look at it and try to trace what order this code executes in.
And what do we have here? A series of state changes that would otherwise be simple imperative code is now... scattered across two async functions, and the only hint about execution order is the "dependency array" at the bottom of each one. In effect, you have to read this code from bottom to top.
I remember when JS promises with their then chains were called clunky, and earlier we already had the "callback hell" problem — but nothing compares to this.
I think these difficulties can be addressed in two ways: a) moving all of this into a separate file, which would only hide the problem; or b) possibly with Redux or something similar, but here I lack enough experience to make specific suggestions.
"Patterns"
All of this together looks terrible and doesn't smell anything like the simplicity that React developers promised in the "Hello world" example. But there's more. The other day I read an article called "The Most Common React Design Patterns." I don't know what I was expecting, but I ended up shocked by the complexity of the patterns described there and how much mental effort is needed just to understand what's happening. And all of this is just to render elements on screen.
And the strangest thing is that the article's author doesn't even acknowledge it. All this complexity is taken for granted. It seems people really do create their UIs this way without complaint.
And for some, even that's not enough — they go further and write "CSS in JS" and even get paid for it. I agree that JSX immediately demonstrated that "separation of concerns" doesn't mean "separation of files," and that it's perfectly fine to write HTML and JS code in the same file. But shoving CSS in there too with strict typing? Isn't that overkill?
Why?
It would be too easy to just declare that React is absurd and go back to our business. But I believe we're rational primates capable of more. We can try to understand the reason.
I dive back into the halls of memory and recall my first job and a colleague who mentioned the jQuery migration project. An incredibly experienced backend engineer, architect, and simply a respected guy in the software world.
What I remember more than his technical decisions was the enormous amount of criticism he expressed toward everything we were doing on the frontend. When he saw some Angular solution, the comment went something like: "What are you even doing here? Can't you make all this simpler somehow?"
And it wasn't about us — our team had decent development experience. It's just that at the time, through the eyes of a backend engineer, the entire Angular system looked completely inadequate.
Today I'm about the same age as that guy was then, and I'm writing an article about how React sucks. I think some things are inevitable.
But let's take a broader view and try to understand why this happened.
I think everyone will agree that most web applications simply shouldn't be web applications. People create a single-page app right away, even when they don't need one right now, simply because it seems cheaper.
But I'll push back here, because in reality this approach creates plenty of hassle. We've just gotten used to the single-page app model as the default and forgotten about simpler alternatives. Launching a plain server-rendered page would be far easier than even thinking about React. In that case, there's no overhead from API communication, the frontend is lightweight, the UI code can be strictly typed (if the backend is also strictly typed), you can refactor across the entire stack, page load speed and caching improve since some components will be static and identical for all users — meaning they can be loaded once. And that's just part of the list.
Although then you won't be able to flexibly implement complex interactive logic at the product manager's demand. But that's not entirely true either. I'm sure you could get quite far by "gradually expanding" your JS code until state management becomes complex enough to justify bringing React into the process.
Okay, I'm saying we use React simply because we used it before. And it's not surprising — inertia is always hard to overcome. But that still doesn't explain such insane code complexity.
And my answer to the question "Why?" — surprisingly — deflects the righteous anger away from React, defending not just this library but also Angular, jQuery, and all their predecessors. I think bad code is explained by the fact that creating an interactive UI where any component can update any other component is simply one of the most complex aspects of software development.
For comparison, think about any other system you use daily. Your kitchen faucet has two inputs — hot and cold water, combined into one output. A kitchen mixer or power drill might have a couple of buttons, and whatever you do with them affects the spinning mechanism. A gas stove might have up to five knobs and the same number of outputs, which already starts to get a bit scary.
But an interactive UI that we implement on the web can potentially have an infinite number of inputs and infinitely many outputs. How can you possibly expect to implement all of this as clean code?
So about all my bashing of React... Essentially, it's not the library's fault at all, just as it's not Angular's or jQuery's fault. Whatever technology you choose, it will inevitably crumple under the unbearable complexity of reactive UIs.
How?
How do we fix this? I'm afraid I lack the knowledge and experience to solve this problem, but I can share a couple of ideas. If we internalize this mental model of "inputs/outputs" on a web page as the foundation, we can start working on reducing the number of those inputs and outputs.
Regarding inputs, I would suggest, for example: "Reduce the number of buttons" — which may not always be feasible. But the connection is obvious — the fewer functional capabilities you have, the easier it is to manage the code.
And this seems obvious enough not to need repeating — right? Do product managers know that adding three buttons instead of two increases bugs by 5% and makes subsequent design and implementation of page behavior 30% more complex? Nobody even measures these numbers, but I think they could very well be accurate.
Why is it that if I tell you we need to add Redis to the backend, you'll push back with "No, we need to contain technical complexity," but if the product manager asks to add a global filter to the app that can be applied from anywhere to everything, you just hang your head and write a code monster that will take another decade to get rid of?
In short, I'm asking you — please stop adding so many buttons. It may sound crazy, but you could even remove some of them.
On the output side, the situation is somewhat different. As I write all this, I realize that creating a server-rendered page essentially reduces that page to a single output. Whatever you interact with, it results in a full page re-render. So, as ironic as it sounds, removing all of React's functional code from the mix actually makes the server-rendered page a pure function of state. If you can afford this luxury, the absence of frontend state provides a significant gain in simplicity.
If you then need scripting logic in such a server-rendered "application," it would be wise to add it only in the most critical places. And the fewer such places, the better.
At first, I thought a good name for this model would be "islands of interactivity." Then I Googled it and found the concept already exists. Nevertheless, that article still mentions Preact, SSR, manifest files — so I'm not sure we're talking about the same thing. People tend to overcomplicate everything.
But I believe that today we have enough bandwidth to load a small React application that renders only an island of interactivity inside a classic server-rendered page. I don't think it would look awkward, but I still need to try it — which I might do in my next project.
So my unverified approach to getting clean and maintainable frontend code sounds like: "Render everything on the server and plug in React only where it's truly necessary."
It certainly won't make things worse.