Hacker Newsnew | past | comments | ask | show | jobs | submit | mpawelski's favoriteslogin

It re-invokes the entire render function of the component so that the state in the component (meaning not externalized to a hook or declared outside of the component) is effectively wiped out.

Vue and Svelte don't work that way.

In a Vue SFC, this is fine:

    <script setup>
    let counter = 0           <-- This code only runs once
    
    watch (someRef, () => {   <-- Only this code runs when someRef changes
      counter++
      console.log(counter)
    })

    const fn = () => { ... }  <-- Normal functions are stable
    function fn2 () { ... }   <-- Stable as well
    </script>
Basically, it behaves like you would expect normal JS closures to behave because the `setup` function is invoked only once. When `someRef` changes, only the `watch` function is executed (this is also why Vue and Svelte perform better because they can execute a smaller subset of code on updates).

JS and DOM itself work with the same conceptual model, right?

    <button onclick="handler()">...</button>

    <script>
    let counter = 0           <-- Allocated once

    function handler() { }    <-- Only this code is executed

    const fn = () => { ... }  <-- Allocated once
    </script>
In React, this (obviously trivial example) doesn't work:

   export const App = () => {
     let counter = 0          <-- New allocation on each render

     const fn = () => { ... } <-- New allocation on each render

     // ...
   }
Because the entire `App()` function gets invoked again. So you have to use hooks to move state out of the path of the component function because the entire component function is re-invoked. Imagine if React, instead of invoking the entire `App()` function, only invoked the code affected by the change as captured in the dependency array. That's how Vue and Svelte work; the same way that a handler function via `addEventListener()` only executes the designated handler function. React does not work this way. Think hard about that and how React is conceptually opposite of how events work in DOM itself.

This difference in design means that in Vue, there is -- in practice -- almost never a need to do manual memoization and never really a thought of "where should I put this"?

It might seem obvious here, right? if `fn` has no dependencies on the component state, then it can just be moved out of the `App()` function. But this is a common mistake and it ends up with excess stack allocations (also why React is generally the poorest performer when it comes to memory usage).

React's model is a bit of an outlier for UIs which generally assume state encapsulated by a component is stable. React says "no, the component function is going to be invoked on every change so you cannot have state in the component."

A different way to think about this is to go look at game engines or desktop UI frameworks which render stateful objects onto a canvas and see how many game engines or UI engines assume that your object is recreated each time. This is certainly true of the underlying render buffer of the graphics card, right? But generally the developer builds on top of stateful abstractions. React has, in fact, moved the complexity onto the developer to properly place and manage state (while not being faster nor more memory efficient than Vue or Svelte).

I believe that a lot of React's expanding complexity flows from this design decision to model inherently stateful UIs in a "pretend everything is a stateless function" way because it is fundamentally playing opposite day against the underlying JavaScript and DOM.


Take this example:

    const { useState } = React;

    function Person(props) {
      console.log('Render Person');
      return (<span>{ props.identity.firstName } { props.identity.lastName }</span>);
    }

    function App (props) {
      console.log('Render Hello');
      const [count, setCount] = useState(0);
      const einstein = { firstName: "Albert", lastName: "Einstein" };
      
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>Increment</button>
          <Person identity={einstein} />
        </div>
      );
    }

    ReactDOM.render(
      <App />,
      document.getElementById('container')
    );
The `<Person />` component will redraw on each `increment`. Nothing changed. Why would it redraw? Because when the `App` component redraws, the `einstein` variable points to a new reference. React sees this as a change and redraws both `App` and `Person`. What looks like normal JavaScript here is not. To prevent this redraw, you have to opt out like this:

    const einstein = useMemo(() => ({ firstName: "Albert", lastName: "Einstein" }), []);
Or know to move the reference outside of the `App` like this:

    const einstein = { firstName: "Albert", lastName: "Einstein" };

    function App (props) {
      ...
    }
Which is fine in this case because there are no dependencies on the component tree. This is the most common mistake I see in React that leads to bugs. It's not just objects, but also functions.

This is also a redraw:

    const { useState } = React;

    function Logger(props) {
      console.log('Render Log')
      return (<button onClick={props.onClick}>Log</button>);
    }

    function App (props) {
      console.log('Render Hello');
      const [count, setCount] = useState(0);
      const logConsole = () => console.log("HELLO, WORLD");
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>Increment</button>
          <Logger onClick={logConsole} />
        </div>
      );
    }

    ReactDOM.render(
      <App />,
      document.getElementById('container')
    );
Why? Because on increment, the `logConsole` is a reference to a new function. So the `Logger` redraws as well. So here, you need to opt out once again by using `useCallback` or moving the function out of the component tree (fine in this case since there are no dependencies). The thing is that it looks like normal JavaScript but the React render cycle is the unseen; you have to be aware of moving things "out of the way" and "bringing them back" via a hook.

React's render cycle re-evaluates entire component sub-trees for changes and if your component doesn't explicitly opt-out by preserving referential equality (`useState`, `useCallback`, `useMemo`, etc.), you'll trigger a redraw downstream. These hooks effectively move the references out of the component tree and pull them back in when the tree re-renders and thus preserve referential equality.

So what teams might do is after experiencing this one time chasing down a bug is wrap every single declaration in a hook to reduce the mental burden. This then creates other issues like performance and memory.

Vue, for example, is the opposite because it has fine-grained reactivity. Nothing redraws until you opt in by using the Vue reactivity primitives.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: