skip to content
usubeni fantasy logo Usubeni Fantasy

Two Critiques of React

/ 3 min read

This Post is Available In: CN EN

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 refs. 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 destroyed 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).

评论组件加载中……