Skip to content

Instantly share code, notes, and snippets.

@tomtheisen
Last active December 7, 2023 16:56
Show Gist options
  • Save tomtheisen/9fbe52f222a6a1ca93f403381239dffe to your computer and use it in GitHub Desktop.
Save tomtheisen/9fbe52f222a6a1ca93f403381239dffe to your computer and use it in GitHub Desktop.
My Beef With React

What's wrong with React anyway?

Here's a partial list of my problems with React. Some may be matters of opinion. There may be more.

Component identity

Reconciliation matches freshly rendered components with stored state. It usually does what you wanted. Setting keys affects the algorithm somehow. I'd prefer explicit component identity to a mostly-correct heuristic.

Stale references from useState

When you call useState(), you get a tuple. [state, setter]. If they had made it symmetrical, it would be a lot harder to get stale state. Specifically [getter, setter]. You could keep the getter forever, and pass it to anything, and it would never need to be resynchronized. Refs are kind of like this, but updates don't cause update renders.

This creates real confusion. Look at this blog post. This guy has confused react's behavior for javascript's.

There’s two huge elephants in the corner of the functional component room: closures and dependencies. We touched on closures in Part One. Closures freeze values at the moment they’re created. Class-based components don’t do that. This is one of the biggest stumbling blocks developers have when they attempt to switch from classes to Hooks.

That's not how closures work.

let x=1; 
const c=()=>console.log(x); 
c();  // 1
x=2; 
c();  // 2

All immutable all the time

All these spreads are inconvenient. Maybe they're slow too, but I haven't measured it. An official react tutorial says it allows for more speed later because change detection becomes reference inequality, rather than brute force object walking. But if you wanted it to be fast, you never would have done it the brute force way. But we're left with these deep spreads. The most direct and obvious model for what a user is doing when they make a change via UI is a mutation. When I click the checkbox, I change the check state. Fighting that simple intuition seems to be a way of pushing the cost of a single implementation detail down into millions of extra lines of application code, forever.

Double dependency list passing

There are some react hooks that take a callback, and a dependency list. For instance useEffect. But the dependencies are in the callback. The dependency list is just redundant. Having two sources of truth creates bugs.

Two way data binding is forbidden

One way data binding is fine. Sometimes it's the best model. Most of the time, even. But like, sometimes you have a text input, and you want a state variable bound to its contents. This is not uncommon or weird. You have to create two separate bindings. One handles the input event. And one pushes the value back into the text box.

As a consequence of this, if you have a component with a "controlled" input, the entire component must be re-rendered for every keystroke. Rendering happens before reconciliation, so the existence of the virtual DOM can't prevent it.

UI is a "function of state"

It's often said that the core principle of react is that UI is a function of state.

Javascript has functions. The functions being referenced in "function of state" are not javascript functions. "Function of x" means a function that takes x as a parameter. Function components are filled with side effects. That's the main (only?) way they achieve interactivity.

Perhaps they'd say "oh, we just meant, like, the general English meaning of the word 'function'". If that's the case, then this is not a React thing. Pretty much every UI I can think of is a "function" of state. It's hard to imagine how you'd make one where this wasn't the case.

The react docs refer to this idea in Keeping Components Pure.

  • A component must be pure, meaning:
    • It minds its own business. It should not change any objects or variables that existed before rendering.
    • Same inputs, same output. Given the same inputs, a component should always return the same JSX.
  • Component functions are supposed to be pure.
  • Hooks may only be called from render functions.
  • Pure functions may not call impure functions.

Therefore hooks are pure functions.

  • Pure functions may not have side effects.

Therefore hooks may not have side effects.

  • Functions without side effects may be called any number of times in any order without affecting program state.
  • Hooks must be called in a consistent order for every render function invocation.
  • Hooks may not be called conditionally.

Therefore hooks cannot be pure functions. We have a contradiction somewhere. Where is it? The answer is not in the documentation.

Legal types for keys

In the typescript typings, only string and number are allowed. Dan doesn't see a use case for symbols. Bigints are also prohibited. I suspect these other types are harder to serialize, and pose problems for re-hydration. But what I really want is to be able to use object references as keys. Like the whole array element. In some cases, an object can have a stable identity that could have been used as a key where there might not be another usable key. This can happen even in purely immutable scenarios, such as array.filter(...).

The call stacks

React's call stacks are giant. That's not a problem by itself. Well, not a big one. They also tend to be asynchronous.

Picture this. Application code creates a react element. Then sometime later, the opaque machination known as "reconciliation" may decide to invoke the render function for that element. Perhaps there's something wrong with one of the props. Good luck figuring out how it got there. The original creator of the element is long gone off the call stack.

How to handle logical events?

In practice useEffect is frequently used to handle changes to state. According to the documentation, this should not be done for the purpose of handling events.

Effects let you specify side effects that are caused by rendering itself, rather than by a particular event.

So how do you handle events created in the business logic of your component? There's a undocumented, experimental hook called useEffectEvent that might be useful. But it's subject to change, being experimental. If you use it the way that's currently considered correct by the documentation, you might need to split up the effect/handler into "reactive"/non-"reactive" parts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment