mgmarlow.com

Why React Matters

by Graham Marlow in javascript, react

A recent popular article compares React to Backbone, arguing that React is not much of an improvement. I think this article profoundly misses the point. And I don't even like React!

Before discussing what the article gets wrong, I want to quickly mention what it gets right. These bullet points captured from the article are a great list of React-isms that are worthy of criticism:

  • key as a mandatory prop for lists is fraught
  • form inputs cannot initialize with undefined, drawing a strange division between controlled and uncontrolled controls
  • useEffect dependency arrays are difficult to understand
  • batched updates lead to awkward APIs, like the callback form of setState

React has many rough edges that are difficult for beginners to wrap their heads around, particularly useEffect and all of the intricacies of dependency management. I can't tell you how many React developers have made the mistake of syncing state via effects when they could simply derive it with a variable assignment.

That said, the article hand-waves over the main reason this complexity exists: the ability to represent UI as a function of state. Or, put another way, unidirectional data flow.

The Backbone example provided by the article clearly demonstrates the lack of this ability. Below is the Backbone render function, tasked with assembling the initial UI of the view:

  render() {
    this.$el.html(`
      <div>
        <input type="password" placeholder="Enter password">
        <div class="space-y-2">
          ${[reqs].map(label => `
            <div>
              <div></div>
              <span>${label}</span>
            </div>
          `)}.join('')
        </div>
      </div>
    `);
    return this;
  }

And the event handler that triggers on changes made to the input:

  updatePassword(e) {
    const pwd = e.target.value;
    const reqs = [
      // ...
    ];

    this.$('.space-y-2').html(reqs.map(([label, met]) => `
      <div class="flex items-center gap-2">
        <div>
          ${met ? '✓' : ''}
        </div>
        <span>
          ${label}
        </span>
      </div>
    `).join(''));
  }

Notice that the event handler is tasked with a localized DOM manipulation. When the input change event fires, the handler queries the DOM and replaces the HTML in-place. The details of the UI are now contained in two different places: the updatePassword event handler and the render function.

This kind of separation is exactly what the pre-React world struggled to maintain. When state is kept separate from the UI, yet requires that the UI is manually updated when that state is changed, complications ensue.

Contrast this with the React solution, where the developer is tasked with constructing state variables (via useState) that update the UI automatically:

const PasswordStrength = () => {
  const [password, setPassword] = useState('')

  const requirements = [
    // ...
  ]

  return (
    <div>
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Enter password"
      />

      <div className="space-y-2">
        {requirements.map((req, idx) => {
          const isMet = req.check(password)
          return (
            <div key={idx}>
              <div>{isMet ? '✓' : ''}</div>
              <span>{req.label}</span>
            </div>
          )
        })}
      </div>
    </div>
  )
}

The most notable difference between the two approaches takes place in the event handler. In React, the handler only updates the underlying component state. The UI is automatically updated by the framework. I think it's hard to argue that this kind of unidirectional data flow isn't a large improvement over Backbone.

tl;dr: LOC comparisons are meaningless.

I think a more interesting comparison is to consider the paradigm of unidirectional data flow outside of React. If I want to render HTML as a function of state, but avoid the complexities of React, what are my options?

Web components are a natural place to look, since they're the HTML standard for UI components. Luckily for us Plain Vanilla Web already provides a comparison against React: The unreasonable effectiveness of vanilla JS.

Notably on the topic of unidirectional data flow, the author of Plain Vanilla Web has this to say:

Because there is no framework patching the DOM with only the parts that changed, we have to tread lightly and only update the DOM when and where that is needed. Recreating too much of the DOM after a state change risks losing state or causing performance issues.

And here we encounter the first rub of implementing unidirectional data flows: re-rendering the entire application as a function of state is expensive! This problem is exactly why React uses a virtual DOM.

If you're willing to adopt a library, there are a ton of compelling options. Lichess famously uses snabbdom, which is effectively a virtual DOM without a framework. If you like the look of HTML template strings, lit-html might be your jam. Or, how about ditching the virtual DOM in favor of a compiler that writes localized DOM manipulations for you?

All this to say, React is a solution built for web applications that are wholly controlled by JavaScript. Server-rendered apps can and should opt for the simpler solution of progressive enhancement because the scope of localized DOM manipulations is much smaller when the server is tasked with re-rendering the HTML. But please, don't use Backbone.