# Everything I Hate About React, I Really Hate About JavaScript Here's a take that might be controversial: The main thing[^1] people hate about React is not React's fault at all, but is actually caused by a design flaw in JavaScript. (Or TypeScript, it makes no difference for the purposes of this post.) Let me show you what I mean. ## The Simple Component That Isn't So you're writing a simple storefront: ```jsx function tore() { const [cart, setCart] = useState([]); return ( <> <ProductListings setCart={setCart} /> <Button onClick={() => checkout(cart)}> Checkout </Button> </> ); } ``` Looks fine, right? Wrong. This component has a subtle but annoying problem. Look at that anonymous function we're passing to `Button`: `() => checkout(cart)`. This creates a brand new function every single render. Which means if `Button` is a memoized component (let's say it's expensive to render for some reason), it doesn't matter, it's going to re-render every single time anyway because React sees a "different" function being passed as a prop. ## The "Solution" That Isn't OK, so React gives us `useCallback` to fix this: ```jsx function ShoppingCart() { const [items, setItems] = useState([]); const handleCheckout = useCallback(() => { checkout(items); }, [items]); return ( <> <Store onAddItem={setItems} /> <Button onClick={handleCheckout}> Checkout </Button> </> ); } ``` Great, problem solved! Except... wait. Now let's say we want to transform our items before checkout. Maybe normalize them to lowercase or something: ```jsx function ShoppingCart() { const [items, setItems] = useState([]); const normalizedItems = items.map(item => item.toLowerCase()); const handleCheckout = useCallback(() => { checkout(normalizedItems); }, [normalizedItems]); return ( <> <Store onAddItem={setItems} /> <Button onClick={handleCheckout}> Checkout </Button> </> ); } ``` Guess what? We're back to square one. That `items.map()` creates a new array every render, which means `normalizedItems` has a different reference every time, which means `useCallback` generates a new function every time, completely defeating its purpose. ## The Cascade of Memoization So now we need `useMemo`: ```jsx function ShoppingCart() { const [items, setItems] = useState([]); const normalizedItems = useMemo( () => items.map(item => item.toLowerCase()), [items] ); const handleCheckout = useCallback(() => { checkout(normalizedItems); }, [normalizedItems]); return ( <> <Store onAddItem={setItems} /> <Button onClick={handleCheckout}> Checkout </Button> </> ); } ``` This is what I call the memoization cascade. You add one `useCallback`, which requires you to add a `useMemo`, which might require another `useMemo` somewhere else. Now our component is literally more memoization boilerplate than actual logic. ## The Real Problem: JavaScript's Equality Here's the thing that really gets me: **none of this would be a problem if JavaScript just had structural equality instead of reference equality**. In JavaScript, `[1, 2, 3] === [1, 2, 3]` is `false`. Two arrays with the exact same contents are considered different because they're different objects in memory. The thing I hate the most about this design decision is that it's literally never what I want. Literally not once in my entire life have I said "I want to tell if these two arrays are referentially equal." I always just want to know if they have the same items. What's the workaround people use in practice? This monstrosity: ```js JSON.stringify(array1) === JSON.stringify(array2) ``` We're serializing data structures to strings to compare them. Think about how absurd that is for a second. The *one* benefit of reference equality was that it was faster than structural equality, because it's just one pointer comparison. So ostensibly in the name of performance, we've ended up in a situation where the only way to check if two objects are equal involves string allocations. Don't get me wrong. Before React, web development was a horrible mess of having to keep track of what parts of the DOM to update on every state change. (And forgetting to update something was a very common source of bugs.) React completely solved that, replaced it with the opposite problem: it's now extremely annoying to help React figure out what _not_ to update. Even if this doesn't causes noticeable performance issues in your app, it's philosophically annoying. The computer is wasting work recomputing things that haven't changed, and the only reason is what feels like a language-level bug. You add one innocent `.map()` and suddenly your whole component tree is re-rendering for no reason. Fortunately for my argument, this are not just performance issues. ## This is a correctness issue Consider this component, intended to notify your server when the user's shopping cart changes: ```jsx function ShoppingCart() { const [items, setItems] = useState([]); const normalizedItems = items.map(item => item.toLowerCase()); useEffect(() => { notifyServer(normalizedItems); }, [normalizedItems]); // oops! this effect will run every render // ... } ``` [This exact issue was responsible for a recent CloudFlare outage](https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/). > The incident’s impact stemmed from several issues, but the immediate trigger was a bug in the dashboard. This bug caused repeated, unnecessary calls to the Tenant Service API. The API calls were managed by a React useEffect hook, but we mistakenly included a problematic object in its dependency array. Because this object was recreated on every state or prop change, React treated it as “always new,” causing the useEffect to re-run each time. As a result, the API call executed many times during a single dashboard render instead of just once. This behavior coincided with a service update to the Tenant Service API, compounding instability and ultimately overwhelming the service, which then failed to recover. > > When the Tenant Service became overloaded, it had an impact on other APIs and the dashboard because Tenant Service is part of our API request authorization logic.  Without Tenant Service, API request authorization can not be evaluated.  When authorization evaluation fails, API requests return 5xx status codes. ## Enter the React Compiler The React team apparently agrees this is a problem, because they invented the React Compiler. This is just a pass over your code that automatically inserts `useMemo` and `useCallback` everywhere for you. I don't know, this feels like an extremely complicated and unsatisfying solution to the problem of equality not doing what it should. But I thought I'd give it a try anyway. Of course, I immediately discovered the React compiler is not an actual solution to this problem. ## The Compiler's Weird Limitations The React Compiler doesn't necessarily optimize every component. If your component already uses `useMemo` or `useCallback`, in some cases it will decide not to optimize that component. So consider the case where you have an extensive computation in a component, and you want to add a useMemo, and you discover that this is a useMemo that the React compiler does not agree with. Now you have a choice: - Remove your legitimate `useMemo` (maybe you're memoizing something genuinely expensive) to let the compiler work - Keep your `useMemo` and remember that this component is now in a separate magisterium, outside the grace of the React Compiler, and you need to manually add all the other memoization yourself Neither is great. Because the compiler doesn't work on all components, you're left in a weird headspace. Either you: - Keep track of which components it optimizes (good luck with that) - Or code as if it doesn't exist and treat it as an occasional performance bonus The second option seems like the only practical approach, which means the compiler doesn't actually save you any effort. You still need to think about when you need memoization for correctness. ## The Root Cause All of this – the memoization cascade, the React Compiler, the performance footguns – stems from one JavaScript design decision: reference equality for functions, objects, and arrays. If JavaScript had structural equality by default, a memoized React component could just check if the new props equal the old props, and skip re-rendering if they do. No `useCallback`, no `useMemo`, no compiler magic. Instead, we have increasingly complex tools to work around a language feature that I've almost never seen anyone actually want. When was the last time you _needed_ to check if two variables pointed to the exact same array in memory, rather than arrays with the same contents? Until JavaScript gets real equality comparison (I think it won't, the tuples and records proposal was withdrawn), we're stuck with this. ## My Suggestion? In my free time, I've been experimenting with a different approach: React for the UI layer and Rust (compiled to WebAssembly) for everything else. The Rust side handles all the actual logic while React becomes just a thin presentation layer. This setup lets me skip most of JavaScript's bullshit. The only thing I have to think about in JavaScript is React and UI stuff, because none of my actual business logic lives there. It's all in Rust. Since I don't have much time to work on this project, I try to be conscious of where I'm spending my time in it. And because in this project I'm constantly switching between the two languages, I've developed a pretty intuitive feel for which one I'm more productive in. And I can say without hesitation: I feel about 10x more productive in Rust, precisely because I'm not fighting the language all the time. It might take slightly longer to get an initial prototype on screen with Rust, compared to if it was all in JS. But the amount of time I spend in JavaScript debugging weird double renders, tracking down performance hiccups, or figuring out why my `useEffect` is running over and over? It's so extreme that I regret every second I spend writing JavaScript and wish I could write the whole thing in Rust. Unfortunately, options for writing websites entirely in Rust are not quite ready at the moment[^2]. But I'm convinced that eventually someone will crack it and create a truly ergonomic way of writing websites in Rust. When that happens, I might never write JavaScript again. If you want to see this approach in action, the app I'm referring to having built this way is [yap.town](https://yap.town). It's a language learning app I'm making to teach myself French (and I'm adding other languages as my friends request them). The entire course engine, spaced repetition system, and sync engine is all Rust. React just puts pixels on the screen based on what Rust tells it. It works pretty well in my opinion (and has allowed me to do some things that would be infeasible from a performance standpoint in JavaScript). ------ (Of course, if you go this route you then have to write Rust, which brings its own problems. It's not my favorite language, but I'm not sure that there's a better non-JavaScript option for deploying to the web, just because of the fact that Rust doesn't need a heavy runtime.) [^1]: Of course, everyone is free to have their own gripes with react. But "`useEffect` is super confusing" is the most common one I see, and the most frequent issue I have personally. [^2]: My opinion, others disagree with me.