Preact internals #1: the easy parts

An introduction and gentle first steps in a series where we’ll read the Preact codebase and try to completely understand it.

About the series

I assume you are already familiar with the React component model and virtual dom rendering. A first React book or online course should be enough background. I also assume you know what Preact is and are interested in it either to make your applications load faster or just because you have a better chance of understanding its implementation than reading the whole React codebase. I won’t try to sell you on Preact, and this is the last bit of marketing in the series:

Preact is the fast 3kB alternative to React with the same ES6 API.

I don’t assume any familiarity with how (P)react might be implemented: learning that is the point! At suitable moments I may also link to the React codebase for comparison, but understanding React’s much more ambitious implementation is way outside of our scope.

We’ll read through the code in roughly the order of its internal dependencies, covering utilities and public interfaces first, and then later diving into the guts that depend on them. For me, the most surprising part of reading Preact’s source was finding how much of it was small, understandable files. And after getting used to that, coming face to face with the two big complicated files that do most of the work. I won’t lie: I got spooked a couple times, but I’ll try to ease into the complexity.

In today’s first installment we’ll cover the really easy parts:

  • Where the code lives
  • Utility functions
  • Global options and devtools integration
  • How JSX becomes virtual nodes

Where the code lives

The interface that Preact implements from React, though quite convenient to use, is fairly confusing to think about implementing. Many of the dependencies and call stacks are circular: components render virtual nodes that describe yet more components, “render” sometimes means generating virtual nodes and sometimes means actually touching the DOM, and the implementation of the Component lifecycle is spread across several different modules.

Yet, for all the complexity of holding this in your head, most of the implementation is made up of small, clear pieces. So before we dive into the code, it might help to try to get the lay of the land:

  • We’ll describe our desired DOM with virtual DOM nodes. Possibly these nodes will be generated by JSX syntax that has compiles into calls to a helper function. We’ll look at the implementation of this step in the last section of this post.
  • Our views can encapsulate stateful rendering logic by inheriting from the Component class. The declaration of that class’s interface and the bits that specific components might override are in src/component.js. But most of the implementation of the lifecycle lives in src/vdom/component.js. Functional components don’t need their own interface, but do have some implementation code in src/vdom/functional-component.js. (If you don’t know the difference between functional and other components, don’t worry: we’ll get to that in the next post in the series.)
  • Preact efficiently renders changes by diffing the virtual dom nodes to the existing DOM structure. Most of that logic is handling in src/vdom/diff.js. When it comes time to actually flush changes to the DOM, it uses helpers from src/dom/index.js. To make rendering faster, it recycles both virtual dom nodes and actual DOM nodes.
  • And along the way there will be a number of other small files with helper functions.

Try to keep that outline in mind. Now we’ll dive in to reading real code with the simplest but most-frequently-imported part of Preact.

Utility functions

The src/util.js file defines helper functions needed elsewhere in the codebase. Defining them locally might seem odd. In an application codebase you might want to just import these helpers from a package like lodash. For a library, though, having our own versions means we can leave out handling edge cases we don’t care about, avoid worrying about dependencies, and make our code as small as possible.

I encourage you to read the whole file. You may recognize many of the functions, like extend and isFunction. Others are worth remembering because we’ll encounter them later on:

  • delve gets nested properties: delve({a: {b: 2}}, 'a.b') == 2.
  • hashToClassName generates string DOM classes from an object mapping classes to whether they are present or not: hashToClassName({a: true, b: false, c: true}) === "a c".
  • defer glosses over browser differences on the best way to schedule something to run asynchronously, as soon as possible.

There: we’ve made it through our first real file in the Preact source, and nothing too bad so far. That’s it for Preact’s utility functions.

Global options and devtools integration

The src/options.js file exposes Preact’s global options. Only a few of these options are useful to regular applications, but they come up a few times in the library’s implementation, and especially in its devtools integration.

What global options does Preact support?

  • syncComponentUpdates sets whether children components are re-rendered immediately when the props received from their parent change. It defaults to true, which is also the default behavior in React. By setting the option to false, you can delay updates to happen asynchronously, on a schedule you determine. This is especially useful if you are doing animations, where you want to update each child once per frame rather than possibly getting bogged down by multiple updates for each child in a single frame. You can see the difference in action with this example of an animating fractal tree. (But be careful: asynchronous rendering may produce some odd behavior, because updates from parent to child happen less predictably. You’ll want to think this through before using it for normal UIs.)
  • Not documented in this file, but useful if you want async rendering, is debounceRendering, which you can set to the scheduling function you want to control rendering. If unset, asynchronous renders will be scheduled using the defer utility function. If you’re playing with asynchronous rendering, requestAnimationFrame is probably what you want.
  • vnode() is a callback function that will be called with every new virtual node created by Preact. afterMount(), afterUpdate(), and beforeUnmount() are callbacks that are passed components at the times you would expect.

These global option callbacks are not intended for use in application code. They’d just make for a maintenance and testing nightmare. But they are very useful for Preact internally to interoperate with the React Devtools, a browser extension that lets you inspect a running React app.

The React Devtools work by integrating deeply with React internals like the shape of components, the interface of the reconciler, etc. Rather then recreating the Devtools for Preact, the integration in devtools/devtools.js works by implementing facades for the bits of React that the Devtools need. The exact details of that code is beyond the scope of this series, since it mostly has to do with React internals. But I do want to point out the initialization code at the bottom, which uses the global options callbacks to do things like normalize Preact vnodes into the expected shape and to notify the interface of changes to rendered components.

In your own applications, you may want to consider setting the options related to asynchronous rendering. But unless you’re working on the Preact codebase itself, leave the global callbacks alone.

With the frequently-imported utilities and options out of the way, let’s look at the first and simplest step in actually rendering things to the screen:

How JSX becomes virtual nodes

Our components need to return virtual nodes to describe the DOM they want to render. Although it isn’t required, the JSX syntax for creating virtual DOM trees is pretty convenient and widely used. Because browsers don’t understand JSX, there are two steps between it and a real, in-memory virtual DOM tree:

  1. Our code is compiled down to plain JavaScript where the JSX is replaced by calls to a function for constructing virtual DOM nodes (or “vnodes”)
  2. At run time, the function calls create a tree of vnodes that describe the HTML we want to render.

Compiling JSX down to plain JavaScript is most commonly done with Babel. To see how that works, here’s an example of code before and after Babel compiles away the JSX:

Notice that our angle-bracket JSX has been turned into function calls of the form:

h(nodeName, attributes, …children)

By default, Babel assumes that JSX will be interpretted by React, so it calls a function calledReact.createElement. Our Preact code won’t have such a function. By placing the pragma /** @jsx h **/ at the top of the file (or configuring it globally in our build process), we can change the function name that’s used. Preact’s vnode construction function is named h , so that’s what we use here.

But what is a vnode? Its definition in src/vnode.js is quite simple: a vnode must have a nodeName, which is either a string (for normal html elements) or a function representing a component. It can optionally also have an object of attributes, an array of children, and a key.

Preact’s src/h.js file exports a function for constructing vnodes that looks complicated but is actually quite simple. It’s easier to understand if I rewrite it in two pieces: first, the overall structure of the file; second, the special-case handling for processing the vnode’s children:

Notice a few special cases in the handling of the children:

  • children can be passed in as extra arguments to h or they can be passed within the attributes passed to h. In the latter case, the children key is deleted from attributes so it isn’t also rendered as a DOM attribute.
  • true, false, and falsy values are skipped. This is useful for writing JSX short-circuiting conditions like { not_logged_in && "You aren't logged in"} that evaluates to false, where you really just want nothing to be rendered.
  • If adjacent children are strings (or numbers that get coerced to strings), they are combined into a single string.

I highly recommend reading the thorough and very legible test cases for this module.

And that’s all it takes for JSX to become virtual dom nodes. Just to review:

  • Babel compiles JSX to function calls. When working with preact, we need to configure it to call the h function.
  • The h function returns a vnode whose children have been massaged into things we can render to the dom, either strings or more vnodes.
  • A vnode is just a description of a dom node: an element name or component, possibly with attributes and children.

Stay tuned for future installments

In the next post, we explore what makes the React component model so useful, explore the differences between stateful and functional components, and build a mental model of how the implementation works.

And then in part three, we look at the DOM manipulation helpers used during reconciliation.