Daniel Moerner

Learning React and Rethinking JavaScript's Warts

There are many ways to do the same thing in every popular programming language, but not all of them are made equal. (I presume this is a corollary of Turing completeness.) This is why it is so important when learning a new language to get a grip on common patterns and anti-patterns. Otherwise we risk making the wrong choices (like assuming that tail recursion is always the optimal recursive algorithm even though with small inputs the time costs of reversing the result may be larger than the benefits in space complexity), or being paralyzed by the thought of too many choices (like I felt when I learned the Node fs module provides three interfaces that all do the same thing).

I’ve spent the last three days starting to learn React, and I want to trace out how this has already led me to question some of my older assumptions about warts and anti-patterns in JavaScript.

JavaScript’s flexibility and overall messiness is well-known. It’s now been seventeen years since the publication of the aptly-named JavaScript: The Good Parts. And on the top of the list of the “Awful Parts” of JavaScript is the reliance on global variables (p. 101). Learning patterns to avoid global variables is one of my defining memories of learning JavaScript. Many of these patterns center around retaining state through the use of local variables in functions, closures, callbacks, and factory functions. For example, you might use a closure to create a counter:

function createCounter() {
    let count = 0;
    return () => ++count;
}

Or you might use a factory function to keep track of state in objects:

function createUser(name) {
    let winCount = 0;
    const getWin = () => winCount;
    const giveWin = () => winCount++;

    return { name, getWin, giveWin };
}

You can even combine these patterns to build a factory function which assigns self-incrementing id’s to new objects, as in this old StackOverflow answer.

Coming from this background, I had the following impression: Support for global variables is one of the worst “features” of JavaScript, but there is ample support for an alternative set of patterns which rely on using local state abstracted inside function expressions.

I felt like my world was turned upside down when I saw that patterns like this do not work in React:

export default function Button() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  return (
    <button onClick={handleClick}>Count {index}</button>
  );
}

In React, each component is recreated on re-render, so each click recreates the index variable and re-initializes it at zero. In a case like this, React is smart enough to not actually change the DOM, because it recognizes that there is no difference between the newly rendered virtual DOM and the current DOM. To achieve a button counter, React offers the useState hook instead (https://react.dev/learn/state-a-components-memory).

It was fairly easy to understand why this doesn’t work in React, but it was harder for me to fit this into my broader understanding of JavaScript. It almost felt like React was not a JavaScript library, but rather an entirely different language with an entirely different set of patterns. What’s going on here?

Here’s the answer that has satisfied me for now: Part of the core paradigm for React is to bring immutability to JavaScript. Components are not modified, they are re-created (re-rendered). Insofar as state is necessary, it must be carefully and explicitly managed with the use of hooks.

Why is immutability so valuable? Because JavaScript is a language built for asynchronous coding. Blocking, synchronous code is unacceptable when users expect their browser to remain responsive while JavaScript code is running. Side effects and mutability make asynchronous code much more difficult to manage. There are also performance benefits, because objects that are immutable can share their shared state via pointer without risk. This is why the React virtual DOM used for reconciliation is so lightweight and can be (or arguably, for access to pointers, must be) stored in memory.

Seen from this perspective, “Global variables are bad” is no longer at the core of the warts of JavaScript. The (ab)use of global variables is actually only an instance of a deeper problem: “There is a fundamental challenge making performant code that simultaneously involves mutability and async”. I believe that this is the deeper problem in JavaScript which React is designed to give us better patterns to handle. And this is what starting to learn React has taught me about JavaScript.

Thank you to Luisa Vasquez, Michelle Bernstein, and Pete Vilter for discussing some of these topics with me in the Recurse Center web development working group.