Back

When should I use preact compat?

📖 tl;dr: Use preact/compat when you are including third-party libraries in your project that were originally written for React.

What's the difference between preact and preact/compat? That's a great and one of the more popular questions if you check out the preact tag on StackOverflow. Preact advertises itself as the thinnest possible virtual-dom 3kB abstraction over the real DOM with a react-like API 🚀. This sentence is quite a mouthful, but the key is the last part: "react-like API".

While the high-level concepts are very similar in any virtual-dom based library, we all implemented them differently. The beauty about this is that as a user you likely won't notice it and can reuse your existing knowledge for building modern Single-Page-Applications (=SPA). This typically includes reyling on several third-party libraries that are available on npm and oh boy are there many of them!

So we looked at ways on how we can leverage existing libraries. We didn't want to put the workload on the community by asking them to rewrite everything for Preact and instead decided to provide a compatibility layer that sits above Preact. That's why it's called preact/compat. So what does the compatibility layer contain that makes libraries written for React seamlessly work with Preact?

The curious case of the Children API

If you ever wrote components that resemble a generic list like an AutoSuggest-component you'll likely want to wrap each list item with something for styling or other purposes. To do this you need a way to iterate over the children and wrap each child with an <li>-tag for example. The thing to watch out for is that props.children is not guaranteed to be an array, so you can't just call props.children.map(fn). When is that the case?

// `props.children` will be an object
<Foo>
<Bar />
</Foo>

// `props.children` will be an array
<Foo>
<Bar />
<Bar />
</Foo>

To solve this React provides a wrapper that can map over props.children in a similar way like you would for an array.

import { Children } from "react";

function AutoSuggest(props) {
return (
<ul>
{Children.map(props.children, child => {
return (
<li key={someId} className="fancy-list">
{child}
</li>
);
})}
</ul>
);
}

Besides the map() function the Children-API supports a few more methods:

API Description
Children.forEach(children, fn) Apply fn on each child, returns void
Children.map(children, fn) Apply fn on each child and return an array of children
Children.count(children) Returns the number of children
Children.toArray(children) Converts children to an array
Children.only(children) Throws when there is more or less than 1 child

If you squint slightly all these methods are very similar to those found on a standard array. They even share the same name in fact. Historically children have always been an array in Preact. This means that you could always savely call props.children.map etc. For Preact X we unfortunately had to change this to support parsing ambiguities with Fragements. Nonetheless we've kept the ease of use of arrays and provide our own method to convert props.children to an array:

import { toChildArray } from "preact";

function AutoSuggest(props) {
return (
<ul>
{toChildArray(props.children).map(child => {
return (
<li key={someId} className="fancy-list">
{child}
</li>
);
})}
</ul>
);
}

The advantage of this is that our API surface remains small. You don't have to learn any new ways to count the items in an array because already do know that. So effectively the Children-API can be replaced with straight array methods in Preact:

API Array Method
Children.forEach(children, fn) arr.forEach(fn)
Children.map(children, fn) arr.map(fn)
Children.count(children) arr.length
Children.toArray(children) toChildArray(children)
Children.only(children) needs to be implemented manually

An argument can be made that creating an array when there is only a single child present is wasteful, but we happily trade that of for a much simpler API. That single allocation is so tiny, that it's extremly difficult to measure even with microbenchmarks.

Unmounting a root node with style

Although it's quite rare, there are some instances where you need to destroy a root node. A root node referes to the topmost node of a tree. It's the one you passed into render() with your component as the first argument.

Adding another function export in our code base is always expensive for size reasons and we always try to avoid adding any new exports. Instead we can leverage the fact that null is alwayst treated as an empty value throughout our whole code base. Because of that we can literally just call render() with null to destroy a root node:

import { render } from "preact";

const App = () => <h1>Hello World</h1>;
render(<App />, document.getElementById("root"));

// Destroy the root
render(null, document.getElementById("root"));

Even our compatibility layer in preact/compat for unmountComponentAtNode does just that:

import { render } from "preact";

// Remove a component tree from the DOM,
// including state and event handlers.
function unmountComponentAtNode(container) {
if (container._prevVNode != null) {
render(null, container);
return true;
}
return false;
}

PureComponent and memo()

Both the PureComponent class and the recently introduced memo() function are ways to specificy a comparison function to potentially skip out of an update. PureComponent is for classes and ships with a default implementation for shouldComponentUpdate. memo() is for functional components respectively and does the same thing. They are both meant to improve performance when the wrapped components are so expensive to render that they effect the user experience negatively.

In our experience this is only true for a very tiny percentage of apps built with Preact. Nearly all performance issues are caused by code that unnecessarily calls setState or forceUpdate. Of course you can always try to solve it with memo or PureComponent but solving the issue at the root is much more beneficial and in my experience easier to reason about. It usually also makes the code easier to read, which is always a plus!

What about forwardRef?

forwardRef is an interesting one, because it's a solution to a problem that we framework authors hope to avoid in the first place. The problem is that the ref and key properties are filtered out of the props object:

function Foo(props) {
console.log(props.key, props.ref); // logs: `undefined`, `undefined`
return null;
}

render(<Foo ref={whatever} key="bob" />, dom);

Even though we passed both properties explicitely to Foo, they won't be available anymore on props. This is caused by createElement filtering out those two properties. forwardRef was brought into existance to pass ref back into a component, because it turns out that forwarding ref properties is very useful for libraries.

import { forwardRef } from "preact/compat";

// Some high-order component
function withLog(Wrapped) {
return forwardRef((props, ref) => {
return (
<Foo>
<Bar>
<Wrapped {...props} ref={ref} />
</Bar>
</Foo>
);
};
}

What if we just didn't delete ref from props? That would make forwardRef completely unnecessary and there even is an open RFC for that over at the react repo. We had this at one point in Preact X before going alpha and filtered it out to stay compatible with React. Naturally we're very excited about making the API surface slimmer and reusing existing solutions ✅.

Personally I think we should go back and only remove ref from props in preact/compat. This should be an easy change to make and would be a good first PR 🎉 With that change in place could simply be rewritten to be a standard functional component, no forwardRef is needed:

// Some high-order component
function withLog(Wrapped) {
return props => (
<Foo>
<Bar>
<Wrapped {...props} ref={props.ref} />
</Bar>
</Foo>
)};
}

Patching in React-specific properties

In my last blog post we talked about using Preact's option hooks to modify our data structures. We use it to add properties that we don't use but many third-party libraries check for. These include $$typeof to check if an virtual dom node was created by createElement, Component.isReactComponent which is just a simple boolean flag and a few more. These just have to be there but aren't really used in Preact.

Conclusion

Phew I wrote more than I intended to but I hope that this quick overview gives you an idea why preact/compat exists and why certain things are not found in the core library. If you feel like something is missing, feel free to get in touch or file an issue on our repo in github 👍

Follow me on twitter, mastodon or via RSS to get notified when the next article comes online.