React Design "Flaws" Compared to Modern Frameworks

Disclamer

Let me start by saying: React isn’t bad. It’s important to frame this discussion as a comparison, not a takedown. The reason we can critique React is because the ecosystem has evolved over time. We’ve found better ways to do things, and that’s worth exploring. React gave us incredible ideas like hooks and JSX, and for that, it’ll always be a pivotal part of modern frontend development. But let’s dive into some of its shortcomings and see how newer approaches might offer a smoother ride.


Opt-Out Reactivity: The First Big Quirk

One of the first things you’ll notice with React is its approach to reactivity. Everything in React is reactive by default unless you explicitly opt out. Sounds nice, right? But here’s the catch: this reactivity is tied to data changes, and that comes with strings attached.

React relies heavily on immutability—a principle borrowed from functional programming. The idea is that functions should be idempotent, meaning the same inputs always give the same outputs. Great in theory, but in practice, React takes this to an extreme. Every time something changes:

  • React recreates elements from scratch.
  • Functions, callbacks, and objects are redefined.
  • There’s no caching by default.

And because of this, React ends up re-executing the component instance. That’s right: every component re-renders. Without careful optimization, this can lead to instability, sluggish apps, and outright broken behavior. You end up manually stabilizing side effects, writing memoization wrappers, and employing performance hacks just to keep things running smoothly.

To put it simply, React’s design makes it far too easy to write bad, inefficient code. Here’s an example:

// Every time the component re-renders, this function is re-created.
const App = () => {
  const handleClick = () => {
    console.log('Button clicked!');
  };

  // ...
  
  return (
    <SomeComponent onClick={handleClick}>Click Me</SomeComponent>
  );
};

Looks harmless, right? But since handleClick is recreated on every render, it’s a new reference each time. If you pass it down as a prop to a child, it could trigger unnecessary re-renders there too. Multiply this behavior across an app, and you’ve got a performance headache.


The Pitfall of Pure Functions

React promotes the idea of pure, idempotent functions. On paper, that sounds like a great way to keep things predictable. But here’s the rub: to do anything remotely useful, you’re going to need side effects. And React leans hard into side effects with hooks like useState, useEffect, and their friends.

Let’s create basic counter:

// Adding side effects with useEffect.
const Counter = ({ initialValue }) => {
  return (
    <button>
      count is {initialValue}
    </button>
  );
};

Everything looks great so far, now let’s create a real counter.

const Counter = ({ initialValue }) => {
  // Hi ! Your function become impure right now : 
  const [count, setCount] = useState(initialValue); 

  return (
    <button onClick={() => setCount(count + 1)}>
      count is {count}
    </button>
  );
};

Now let’s add a useEffect to access the DOM (the main purpose of using JS in the browser, by the way)

const App = () => {
  const [count, setCount] = useState(0);
  
  // The "fun" start here
  useEffect(() => {
    const counter = document.querySelector('#counter');
    console.log(counter.innerText);
  }, [count]);

  return (
    <button id="counter" onClick={() => setCount(count + 1)}>
      count is {count}
    </button>
  );
};

Here, we’re introducing a side effect to log changes whenever the count updates. While this is a trivial example (that’s pretty dumb), real-world apps often involve a maze of useEffect calls to handle state synchronization, API calls, subscriptions, and so on. Suddenly, your “pure” functions are filled with side effects, and keeping them stable becomes a juggling act.


“Both Worlds”: A Contradiction

Now, let’s talk about a contradiction in React’s design. It tries to apply the paradigm of immutability from functional programming in contrast of object-oriented (OOP) approach that mutate on reference. OOP feels natural but has the downside of generating lot of side effect, Immutability provide stability with copying every object as a tradeoff. But instead of getting the best of both worlds, React ends up with the downsides of each:

  1. Immutability Trade-offs: Copying data every time something changes leads to performance overhead. React constantly recreates objects, functions, and elements.
  2. Side Effects Everywhere: React’s reliance on hooks to introduce side effects creates complexity. Instead of naturally integrating with the flow, you have to stabilize them manually.

Imagine this:

// This button component looks simple but re-renders unnecessarily.
const Button = ({ onClick }) => {
  return <button onClick={onClick}>Click me</button>;
};

const App = () => {
  const handleClick = () => {
    console.log('Clicked!');
  };

  return (
    <div>
      <Button onClick={handleClick} />
    </div>
  );
};

Without memoization, the Button component re-renders every time App re-renders, even if nothing changed. Why? Because handleClick is recreated on each render. These issues stem from React’s architecture, and fixing them involves extra work like useMemo or React.memo.


“It’s (Almost) Just JavaScript”

Another big selling point of React has always been its closeness to JavaScript. JSX, for example, is just syntactic sugar for writing JavaScript—great for developer experience (DX) but with zero direct impact on app performance. But here’s the thing: JavaScript itself isn’t a reactive language.

This limitation means React has to rely on a lot of boilerplate and conventions to make reactivity work. Ever heard of the “Rules of React”? Things like putting hooks at the top level and using dependency arrays. These rules exist because React’s reactivity doesn’t come naturally from the language—it’s patched on top of it.

React’s team recognized this overhead and started working on solutions, like the React compiler. The idea? Reduce the burden by leaning into optimizations, similar to what frameworks like Svelte do. But there’s only so much you can do when you’re working with primitives. It’s an uphill battle for a compiler to optimize React components when the underlying system isn’t inherently reactive.


Modern Alternatives: Vue and Solid

Now, compare that to modern solutions like Vue and Solid. These frameworks tackle reactivity with a more granular approach:

  • One-time execution: Components are created once and executed only when necessary.
  • Zones of creation: A clean space where everything is initialized and doesn’t need constant recreation.
  • Granular execution: Using signals and dependency tracking, they only update what truly needs to be updated.
  • Reactive primitives: Instead of relying on JavaScript primitives, these frameworks introduce strong reactive systems that are easy for compilers to optimize.

Because of these design choices, modern frameworks like Vue and Solid can implement advanced optimizations like hoisting and reactive caching almost effortlessly. Developers don’t have to constantly think about stabilization, memoization, or dependency management. The frameworks handle it for you, which translates to cleaner, more efficient code.


Closing Thoughts

React brought us a lot of good ideas. Hooks and JSX alone changed the way we think about component-based development. But as the ecosystem evolves, its flaws become more apparent. The heavy reliance on immutability, the constant recreation of components, and the manual optimization required all feel clunky compared to the seamless reactivity of frameworks like Vue and Solid.

The good news? React’s legacy means it’s continuously improving. With initiatives like the React compiler, it’s clear the team is aware of these issues and working toward solutions. But for now, if you’re looking for a framework that just works—with built-in reactivity and better performance out of the box—it’s worth exploring the alternatives.

After all, the beauty of modern development is that we have options (maybe too many). And every iteration brings us closer to frameworks that make building apps a joy rather than a chore.