Preact internals #2: the component model

What distinguishes the React component model, the difference between functional and class-based components, and an overview of how it all works.

In the first post in this series, we started to build a mental model of the Preact codebase, explored some fringe code for utilities and global options, and saw how JSX becomes virtual dom trees. In this second post, we’ll look at the (P)react component model: what it is, how functional and class-based components work, and how the implementation is structured.

What (P)react is really about

Let’s start by dismissing a major misconception about the React UI model (and the parts of it implemented by Preact). I still frequently read that the main benefit of React is the performance benefit of using virtual DOM diffing to render HTML. Now, virtual DOM diffing is neat, but it’s just an enabling feature for React’s core idea, which is its component model.

  • Stateful components are aware of their history and lifecycle. When authoring a component, it is frequently useful to be able to compare current data with previous, to store local state hidden from the parent component, and to run code when an instance is first created or finally destroyed.
  • Declarative components describe their contents with simple data structures rather than stateful instances. When a declarative component’s data is updated, it returns a brand-new specification of what should be rendered. It doesn’t have to remember previous child components, old versions of their data, or destroying them when no longer used.

Component types: class-based and functional components

The React component model has two types of components: objects that inherit from the base Component class and simple functions. As of now, functional components are always stateless, just receiving props and context as arguments and returning a virtual DOM node:

let Person = (props, _context) => 
<div>{props.name}</div>
class Person extends Component {
render(props, _state, _context) {
// In React, props/state/context are not passed as arguments,
// but accessed on `this`. Preact adds them as arguments too.
return <div>{props.name}</div>
}
}

The Component base class

The component above was stateless, just returning a vdom tree for its props. But we can also use class-based components to add local state and handle lifecycle callbacks. (The best guide to the full Component API is the React docs.) In this section, we’ll read through the declaration of the Preact Component class that your stateful components can extend.

  • setState merges new state values into the component’s existing state, then queues the component to be rendered asynchronously. (Local state changes, unlike changes to props passed from a parent, are always rendered asynchronously.)
  • forceUpdate causes a synchronous render of the component.

Implementing the Component lifecycle

So far, we’ve described what the runtime does for us, and seen the stateful class that our components extend. But how does the runtime work? There is a lot of complexity to the implementation, so before reading the code let’s build a picture of the functions involved and what each of them does.

diff(dom, vnode, domParent)
idiff(dom, vnode) mutates the DOM
innerDiffNode(dom, vnodes) pairs & diffs children
setComponentProps sets instance props
renderComponent sets component content & calls lifecycle methods
  • Pairing DOM children to vnode children and how key works
  • Crazy DOM special cases, like event binding and XML namespaces
  • Recycling unused components and DOM nodes for performance
  • Hydration, mount and unmount callbacks, and refs

Appendix: further reading in UI programming

In my description of the React component model, I may have misled you into thinking that it is an absolutely new and singular idea. This is completely untrue. If you are interested in other approaches to UI programming, you might be interested in:

  • Some bleeding-edge tech: In the first post of the series, we saw Preact’s basic support for asynchronous rendering, currently a major area of exploration. The original React model renders an entire DOM tree at once. If we could instead break up and prioritize the work, we might be able to quickly render the most important content, or update short animations, rather than allowing them to be slowed down by less-important content. React Fiber is a reimplementation of React with a concurrency model that makes it possible to do chunked, prioritized, and speculative rendering. Get started by watching Lin Clark’s amazing talk “A cartoon intro to Fiber.” And expect to hear a lot more about Fiber in the next year.
  • A totally-different programming model: Elm is a statically-typed, pure functional language for building web UIs. There is a thorough and well-written guide to Elm UI architecture, which makes a strong case for types and purity in UI development. Individual Elm components cannot encapsulate local state or behavior. This enables some amazing tooling, like a time-travelling debugger that always works. But it also poses a barrier to certain kinds of component encapsulation and reuse. If you are conversant in Haskell and want to see how typed functional components can have local state at the cost of some interesting type signatures, you might explore PureScript’s Halogen.

Building the web. Everything should be faster.