Two Critiques of React
/ 3 min read
React’s philosophy is often touted as comprehensive and well-thought-out—I’ve even summarized parts of it myself. Some of React’s design decisions are indeed quite clever, but that’s only one side of the story.
In certain situations, these philosophical justifications feel more like “adaptive reasoning” for React—in other words, finding excuses to cover up React’s design flaws. In some scenarios, these design issues become extremely difficult to work around, or even impossible to bypass. No wonder people say React has a high mental overhead.
Event Dependencies
It’s well-known that useEffect dependency management is notoriously difficult, and this particular issue represents one of the most challenging branches of that problem.
For example, consider a non-React native library that you want to wrap as a React component.
viselect/packages/react/src/SelectionArea.tsx at master · simonwep/viselect

You can see that the author didn’t include all the values used inside useEffect
in the dependency array, treating it as initialization code. This is usually fine, but what if your props.onStart
references a useState
variable?
Then you’re screwed. It won’t work because when that state updates, this useEffect
won’t re-run at all, so you’ll always get the initial value.
Check out this example, pay attention to the highlighted area:

What if you add props.onStart
to the dependencies? Sure, that would work, but it means the entire initialization process would run repeatedly, which is completely pointless—like you think the user’s computer has too much performance to spare.
So what’s the solution? You could replace all the changing values used in the function with ref
s. This works, but it’s quite ugly. Let’s continue.
There’s actually a more official and elegant approach: using useEffectEvent—at least that’s what it’s called for now, since it’s still an experimental feature as of React 19’s release.
Functions inside useEffectEvent
don’t need to be listed in dependencies, yet all the values they use remain up-to-date. This is primarily designed for the “event handling” scenarios described above.
Here’s the example code from the official documentation:
function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); });
useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); // ✅ All dependencies declared // ...
Cleanup Order
useEffect() parent-child cleanup order · Issue #16728 · facebook/react
useEffect
cleanup looks elegant—all related code is cohesively contained within a single function.
However, React’s cleanup order for side effects is somewhat arbitrary. It’s possible that a parent component’s cleanup function has already destroy
ed something you need, while the child component’s cleanup function still tries to use that destroyed object, leading to bizarre errors. This is yet another point where React is unfriendly to non-React native library integration.
Moreover, the React team doesn’t even plan to fix this issue. In most cases, you can ignore it at the risk of memory leaks, but if you absolutely must solve it, you’ll need to write your own workaround to handle global destruction.
So
While React is indeed useful, in certain situations—such as when you’re using non-React native UI or event libraries—it’s genuinely not so great (stating the obvious, I know).