React brain rot is not just about React. It is about a way of thinking that makes developers forget what the browser already understands, what the server already knows, and what architecture was supposed to mean in the first place.

I know the title is aggressive. I mean it that way. I have spent enough time dealing with over-engineered JavaScript systems, empty client-side shells, hydration layers, frontend permission theater, duplicate API calls, and folder-inside-folder component bloat to say this plainly: a lot of SPA architecture is not architecture. It is learned waste.

The main issue I have with most SPA designs is twofold.

First, a single-page application is not really an architecture in the same sense that MVC or HMVC is an architecture. React, Vue, Angular, Svelte, and the rest may be built by smart people using real engineering, data structures, scheduling, diffing, rendering engines, compilers, and bundlers. That does not mean the application developer using the framework is being forced into sound architecture.

Most of the time, the framework gives the developer a magic box and says, "Put everything here." Then the developer blends HTML, CSS, JavaScript, data loading, state management, routing, and permission checks into a component tree and calls it separation of concerns.

I do not buy that.

Editorial comic of a frustrated brown software builder with spectacles pointing at simple HTML while a bloated React and SPA architecture tower looms nearby
Fourteen reasons later, I still do not understand why so many teams reach for React and SPA architecture by default.

1. Separation Of Concerns Should Mean Something

The SPA crowd loves to talk about separation of concerns. The frontend is separate from the backend. The API is separate from the UI. Components are separate from each other. Everything is supposedly clean.

Then you open the code and see HTML-like markup, CSS classes or inline styling, JavaScript logic, event handling, state management, data fetching, and conditional rendering all jammed into one JSX block.

That is not separation of concerns. That is a pile of concerns wearing a component costume.

True separation of concerns is much simpler:

  • HTML gives the document structure.
  • CSS gives the visual style.
  • JavaScript gives the interactivity.
  • The server owns access control, routing, data loading, and the initial document.
  • Data should be rendered with precision exactly where it needs to appear.

That is separation of concerns. Combining everything into JSX and saying, "We separated backend logic from frontend logic," is not impressive to me. It is a very narrow definition of separation that ignores the mess created inside the frontend itself.

For a page to load cleanly, the rule of thumb should be simple: separate structure from style from interactivity first. Ask what the page actually is. Is it mostly a form? A table? A dashboard? A blog post? A settings page? A report? If the page is mostly static structure with basic interaction, then render it as static structure. The server can easily combine templates, data, and CSS classes into clean HTML. Then ES6 can sit on top as a script tag and enhance the parts that need behavior.

If one part of the page is sophisticated, let JavaScript own that part. If a chart, editor, drag-and-drop zone, live preview, or canvas widget needs richer client-side behavior, fine. Make that part interactive. But do not turn the entire application into a client-side operating system because one component needed interactivity.

This is the mistake I see everywhere. Five percent of the page needs real client-side behavior, so the team forces one hundred percent of the page through a frontend framework. The static content, layout, forms, tables, routing, loading state, and access state all get dragged into the same SPA runtime. That is not separation of concerns. That is pigeonholing the whole application into the most complicated path because one corner of it needed JavaScript.

There are deeper details here: how to structure HTML, CSS, JavaScript, data mapping, templates, and runtime state cleanly. That is a topic for another day. But the basic principle is simple: let static things be static, let the server render what the server already knows, and let JavaScript enhance only the parts that actually need to become interactive.

The browser understands HTML, CSS, and JavaScript. It does not understand React. It does not understand TypeScript. It does not understand JSX. All of that eventually gets compiled, bundled, transpiled, hydrated, and shipped back into the same three things the browser understood from the beginning.

2. The Button Should Not Need A Cathedral

My second issue with React brain rot is the design culture it creates.

To render a button, you often get a component for the button, a folder for the button, another folder for the parent wrapper, another file for the nested div, another file for the style variant, another hook for the state, another utility for the class names, another test file, another Storybook entry, and another import chain.

All of this to eventually produce something the browser could have understood directly:

That is all the browser needs.

The browser does not need your folder structure to render a blue button. It does not need a component hierarchy to understand a click. It does not need a build pipeline to know what a

That is the thing the browser wanted. It did not ask for hydration. It did not ask for a node_modules folder. It did not ask for a transpilation pipeline. It did not ask for dependency hell. It did not ask for a 3 MB bundle before showing a page whose primary job is to display text and collect a form submission.

This is the part that drives me nuts: developers build a dependency-heavy toolchain to produce what the browser already speaks natively, then call the result modern.

5. The Lies Of Virtual DOM Being Fast

Then there is the weird marketing line that React is fast because the virtual DOM is fast.

I will tell you what is fast: the browser rendering this directly:

No virtual DOM. No component tree. No framework bundle. No hydration. No diffing. No reconciliation. No runtime that has to load before the button can become interactive.

The browser already has a rendering engine. It is extremely optimized. It knows how to parse HTML, apply CSS, build the DOM, paint the page, and handle events. When all you need is a button, a form, a table, a tab, or a page of content, going through a framework runtime first is not automatically faster. In the simplest case, it is obviously more work.

React can feel fast on modern computers because modern computers are absurdly powerful. That does not mean the architecture is fast. It means your machine is powerful enough to hide the waste.

Open a browser process view or a task manager and look at memory and CPU usage across tabs. The heavy React apps are usually near the top. They may still feel usable because your laptop has enough cores, memory, and battery to absorb the cost. But that is not the same as being lightweight.

So no, the virtual DOM is not some universal speed miracle. For complex interfaces with lots of changing state, a virtual tree and reconciliation strategy may be a reasonable tradeoff. For rendering a blue button or a server-known page, loading a framework so the framework can build a virtual representation of what the browser could have rendered directly is not faster in any honest definition of the word.

6. React Culture Turns Refactoring Into Folder Moving

Another thing I dislike about React culture is that no two React developers seem to agree on what a React codebase should look like.

Comic of developers complaining about a previous developer while adding folders inside folders to render a simple button
The refactor often becomes folder-moving theater around a browser primitive that already worked.

One developer works on the project for six months. Another developer joins. The new developer says the previous developer wrote terrible code. The project needs a refactor. Then the refactor is mostly moving folders around, renaming components, splitting files, introducing another pattern, and adding more ceremony to render the same interface.

I have not seen many architectures where moving folders around is the architecture. But in React land, a lot of development feels exactly like that.

The problem is not always that the previous developer was bad or the new developer is better. The problem is that the stack does not impose enough architectural discipline. React gives you a rendering model. It does not give you MVC. It does not give you HMVC. It does not give you a server-side controller/action lifecycle. It does not give you a real application boundary. It does not give you a single place for access control. It does not force data to flow from a real backend architecture into a precise view.

So every team invents its own architecture on top of React. Then the next developer dislikes that invented architecture and invents another one.

This is why the same complaints appear everywhere. I can talk to developers in one country and hear the same React complaints I hear somewhere else: the state is messy, the components are messy, the folders are messy, the previous developer made bad choices, the app needs a rewrite.

At some point, maybe the common denominator is not the last developer. Maybe the common denominator is the stack and the culture around it.

7. Duplication Is Built Into The SPA Mindset

The biggest architectural sin is duplication of logic.

Comic of a team claiming separation of concerns while duplicating ACL routing feature flags auth and workflow logic across frontend and backend
Calling it separation of concerns does not make duplicated ACLs, routes, flags, auth, and workflow rules disappear.

SPA advocates often begin with the claim that separating frontend and backend gives clean separation of concerns. Then, in practice, the same logic appears in two places.

Access control is a good example.

In an HMVC or MVC application, there is a request lifecycle. Before the controller action runs, a security layer can check whether the current role has access to the controller and action. If access fails, the action does not run. If access passes, the controller loads the data and renders the view.

That is clean. One robust place. One request lifecycle. One source of truth.

In a React SPA, the page often needs its own permission logic because the frontend owns routing. So now you have route guards, component-level permission checks, hidden buttons, permission endpoints, and backend API checks. But the frontend check is not real security. A user can bypass it. The backend must still check everything.

So what did the SPA architecture give you?

Duplicate permission logic.

More attack surface.

More bugs.

More places to keep in sync.

And somehow this is called clean architecture.

8. Routing And Workflow Logic Get Duplicated Too

The same problem shows up in routing and workflow state.

Take a shopping cart. A user should not be able to pay if there are no items in the cart. That rule belongs on the server because the server is the system of record. The server knows the cart, the session, the user, the payment state, and the checkout requirements.

But in an SPA, the frontend also needs to know the flow. It needs to decide whether to show checkout, whether to redirect, whether the user can move from one step to another, whether payment should be enabled, and whether the UI should block a route.

Then the backend has to check the same thing again because the frontend cannot be trusted.

So now the app has two versions of the workflow:

  • The frontend version, used to make the UI feel correct.
  • The backend version, used to actually protect the system.

This is the duplication problem again. It shows up in ACLs, routing, checkout flows, onboarding flows, state machines, feature gates, and business rules.

A server-rendered MVC or HMVC system can put that logic in the request lifecycle. A plain server-rendered application can do the same thing without pretending the browser is the source of truth. The server decides whether the user can enter a page. The server renders the correct state. JavaScript enhances the interaction.

9. React-First Design Pushes Microservice Bloat

The problem with React is not only the React application itself. The deeper problem is that once you choose a React-first architecture, the whole system design starts bending around it.

Comic of a bloated SPA tower forcing many containers for admin ecommerce blog SEO auth flags routes and logs
Once the frontend becomes the center of the architecture, the rest of the system starts orbiting around it.

Say your product needs a dashboard, ecommerce, admin tools, and a content/blog section. In a full-stack MVC or HMVC system, these can be separate modules inside one coherent application. If one controller action breaks, the whole application does not become a blank screen. If one module has a bug, the other modules can still render. The browser still receives HTML. The broken part is isolated by the server-side architecture.

In React land, the pressure often goes the other way. If you put dashboard, ecommerce, admin, and content into one giant client application, the bundle grows, the state surface grows, the routing surface grows, and one broken build or runtime error can threaten the whole interface. So teams start splitting things apart.

Content marketing becomes a separate app because React is painful for SEO unless you add another rendering layer. Admin becomes another app because you do not want admin bugs taking down ecommerce. Ecommerce becomes another app because checkout has its own risk profile. Then each app becomes its own container, its own deployment, its own routes, its own ACL rules, its own feature flags, its own environment variables, and its own maintenance burden.

Now the team has "separation of concerns" by running a small village of containers.

That is not free. Every container consumes resources. Every service needs deployment, monitoring, routing, logging, secrets, health checks, security patches, and ownership. If you keep adding containers because the frontend architecture cannot comfortably contain the product, go look at the EC2 bill, the Kubernetes bill, the logging bill, and the engineering payroll needed to keep the whole thing alive.

Worse, the duplicated logic returns. Each app needs to know routes. Each app needs to know permissions. Each app needs feature flags. Each app needs auth state. Each app needs to coordinate with backend APIs. The same ACL and workflow rules spread across frontend apps and backend services again.

This is why React is not "just a frontend framework" once you build around it. It can become the gravitational center of your entire architecture. The frontend choice starts influencing service boundaries, deployment topology, SEO strategy, auth design, routing, caching, and team structure.

A simple full-stack architecture can often handle the same product with less drama: modules, controllers, actions, views, shared security rules, and server-side rendering. JavaScript can still enhance the UI. But the frontend is a second-class citizen in the right way: useful, powerful, and interactive, but not the thing that holds the entire system hostage.

If we go down the security rabbit hole, this gets even worse. More services, more clients, more duplicated route guards, more tokens, more cross-service permissions, more exposed endpoints, and more places to make mistakes. That is a topic for another day, but the attack surface does not magically shrink because the architecture has more boxes on the diagram.

10. The Stupid Double Fetch Problem

The pattern that annoys me most is simple: the server already has the data, but the browser fetches it again because the frontend stack trained everyone to do that.

You can see this in countless applications. The request comes in. The backend checks the user. The backend loads the record. The backend has the title, fields, status, permissions, settings, and whatever else the page needs. Then instead of rendering that data directly into the page, the system ships an empty shell or a thin config object to the browser.

Then the JavaScript boots, calls an API, waits for the same data, stores it in component state, and finally renders what the server could have rendered in the first response.

async function bootPage() {
    const response = await fetch("/api/records/123");
    const record = await response.json();
    renderPage(record);
}

The React tutorial pattern trains this behavior:

function App() {
    const [data, setData] = useState(null);

    useEffect(() => {
        fetch("/api/data")
            .then(r => r.json())
            .then(setData);
    }, []);

    if (!data) return 

Loading...

; return

{data.title}

; }

After writing that pattern enough times, developers stop asking the important question: did the server already load this data before it rendered the page?

The simpler pattern is load once, render once. The server loads the data. The template renders useful HTML. If JavaScript needs the same data for interaction, embed the runtime object once and use it directly:

class PageController {
    constructor() {
        this.data = window.pageData;
        this.bindEvents();
    }
}

That is it. No mount-time fetch. No second database query. No fake loading state. No architecture diagram needed to justify waste.

One page load should not do two pieces of work for the same data:

  • The server loads enough information to decide what page to render.
  • The browser then calls back into the server to fetch the same page data again.
  • The user waited for a second round trip before the UI became useful.

The application flow should be boring:

  1. The request reaches the server.
  2. The server checks access before rendering.
  3. The server loads the data.
  4. The template renders complete HTML and embeds the data needed for interaction.
  5. JavaScript enhances the page.

This is not anti-JavaScript. It is anti-stupidity. JavaScript should enhance the interface, submit changes, handle local interactions, and make the page smoother. It should not be forced to reconstruct the page from scratch when the server already knew what to render.

That is React brain rot. It is not stupidity. It is training. It is muscle memory. It is the repeated lesson that every page begins empty and every component earns its right to exist by calling fetch() inside useEffect().

11. The Server Is Not A Dumb API

The deeper disagreement is philosophical.

The SPA mindset says:

Server equals API. Client equals application.

The server-rendered mindset says:

Server equals application. Client equals enhancement.

That one change removes a surprising amount of nonsense.

This is why I care about ANTSAND's design. ANTSAND is not just server-rendered HTML. It goes deeper into separation of concerns. Data can come from Notes, Forms, Sales, Resume, Databoards, website settings, groups, assets, or raw JSON. The data mapper decides what feeds a section, but it does not get to randomly mutate the HTML structure. Section templates define header, body, and footer contracts. CSS classes, Sass, and sass_v2 style those contracts. JavaScript enhances the rendered page as a second-class citizen when needed.

ANTSAND Databoard UI showing website sections, JSON mapping, and generated page structure
Databoard is the ANTSAND UI that maps data into website sections and renders generated websites, including this blog.

Databoard is the best example. It is a complex application, but much of its sophistication comes from strict contracts: mapped data, fixed section structure, reusable renderers, compiled Sass, and ES6 behavior layered on top. That is real separation of concerns. Not JSX mixing structure, logic, styling, fetching, and state into one component and calling it clean.

Instead of shipping an empty shell and asking the browser to assemble the page, the server sends a complete document. Instead of hiding the actual data behind layers of state, props, fetch responses, hooks, and dev tools, the page can expose the data directly for debugging when appropriate.

If I open the console and type:

pageData

I should see the real data. Not a guessed TypeScript interface. Not a collapsed component prop. Not a response buried in the network tab. Not some React DevTools view invented to inspect problems React created. The actual runtime object the page is using.

ES6 debugging can be brutally simple. Put the object on window when appropriate. Use console.log(). Inspect the DOM. Read the network tab. Step through the actual JavaScript the browser runs. You do not always need another framework-specific devtool just to understand why a title or button did not render.

If the data is sensitive, do not send it to the client. But if the user is authorized to see it, pretending it is hidden because it lives inside React state is nonsense. The network tab already exists.

12. React Was Built For Facebook's Problems

This is the part people miss. React makes a lot more sense when you remember where it came from.

Facebook had Facebook-scale problems. If you have hundreds of millions or billions of users hammering your systems, then moving more rendering work to the client can be a rational tradeoff. Saving a small percentage of bandwidth or server compute at that scale can matter. If the choice is between building another data center or pushing more UI work to the user's machine, the tradeoff changes.

At that scale, some duplication can be justified. Some architectural ugliness can be justified. Some client-heavy complexity can be justified. Facebook has the engineering headcount, infrastructure, profiling, testing, and operational discipline to harden that complexity because the payoff can be massive.

But most teams do not have Facebook's problems.

Most teams are not serving billions of users. Most teams are not saving enough bandwidth to change data center planning. Most teams are building forms, dashboards, content sites, admin tools, internal systems, ecommerce flows, and SaaS screens. For those teams, blindly adopting Facebook's client-heavy tradeoffs often creates more complexity than value.

The crossover point where React's complexity is justified is much higher than people admit.

React may be perfect for Facebook's use case. That does not mean it is perfect for your blog, your admin panel, your checkout flow, your settings page, or your CRUD dashboard.

13. Hydration Is Often Just Extra Steps

Hydration is often sold as the answer to SPA performance problems. The server renders HTML, the browser downloads JavaScript, React re-executes the component tree, attaches event handlers, and everyone celebrates because content appeared earlier than a pure SPA.

But in many cases the browser is still paying for a second application runtime to rediscover a page the server already rendered.

That may be necessary for some applications. It is absurd as the default for a blog, a notes site, a marketing page, a checkout page, or a simple admin flow.

Hydration is not automatically architecture. Sometimes it is React brain rot with extra steps.

14. Why This Matters More With AI Coding

This matters even more now because AI coding agents are very good at copying the dominant pattern in a codebase or ecosystem.

If the common pattern is React SPA overuse, the agent will generate more of it. It will create API endpoints, loading states, frontend guards, state machines, hydration workarounds, and client-side render loops because that is what the training corpus says modern web development looks like.

That is why taste and architectural discipline matter. If I do not actively enforce the load-once pattern in my own systems, AI will happily reintroduce the same waste I am trying to remove.

AI makes it easier to write code. It does not automatically make the code coherent. In fact, AI makes architecture more important because bad defaults can now spread faster.

15. The Rule I Want

The rule is simple:

If the server already has the data during page render, do not fetch it again on page load.

There are exceptions. User-triggered refreshes are fine. Autosave is fine. Live updates are fine. Mutations are fine. Highly interactive browser-native tools are fine. But initial render should not perform a fetch just because React tutorials trained everyone to do it.

Load once. Render once. Enhance with JavaScript.

That is the pattern I want hardened in the systems I build. In ANTSAND, that means server-rendered HMVC, precise data mapping, and JavaScript that enhances pages instead of pretending every page is a miniature operating system.

16. Conclusion

React brain rot is a thinking problem. It teaches developers to make the browser rediscover what the server already knew. It makes loading states feel normal. It makes duplicate API calls feel clean. It makes hydration theater feel sophisticated. It turns backend security into frontend permission cosplay. It creates dependency bloat, folder-moving refactors, and duplicated workflow logic. Then the ecosystem sells more tooling to manage the complexity it created.

I do not want that in the software I build.

The server should own access control, data loading, routing, workflow state, and the initial document. The browser should enhance what the server sends. That is faster, simpler, easier to debug, and far less annoying than pretending every page is a tiny operating system that must boot inside the browser.

React does not solve every problem. Sometimes it shifts the problem into the browser, doubles the work, and calls the result modern.

I am not interested in modern if modern means slower, heavier, harder to debug, and architecturally confused.