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 👍