Preact X is here 🎉
Preact X Alpha 0 is here!
tl;dr: Preact X is the next major release which comes with a plethora of highly requested features like Fragments
, componentDidCatch
, createContext
, hooks
and many compatibility improvements with third-party libraries.
Fragments ✅
Fragments
has been the most requested feature for Preact for a long time and we were very keen on bringing them into Preact! With Preact X they are now finally here 🎉 Use the new Fragment
export in your components to render children elements inline with their parent, without an extra wrapping DOM element. For example:
import { render, Fragment } from "preact";
function Table() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
function Columns() {
return (
<Fragment>
<td>Hello</td>
<td>World</td>
</Fragment>
);
}
render(<Table />, document.body);
// Resulting DOM:
<table>
<tr>
<td>Hello</td>
<td>World</td>
</tr>
</table>
You can also return arrays from your components:
function Columns() {
return [
<td>Hello</td>
<td>World</td>
];
}
Don't forget to add keys to Fragments
if you create them in a loop:
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// Without the `key`, Preact can't efficiently add,
// remove, remove new elements as the list changes
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
</dl>
);
}
Fragments are a major new feature of Preact X that largely motivated the rewrite
from Preact 8. We could really use a lot of help testing and validating our
Fragment implementation. If you find something that doesn't seem right, we would
really appreciate reporting a minimally reproducible example to help us improve
our implementation and messaging around Fragments
.
componentDidCatch ✅
We all wish errors wouldn't exists in Web-Apps but sometimes they do happen. With componentDidCatch
all errors that happen inside render
or another lifecycle method can be caught. This can be used to display user-friendly error messages, or write a log entry to an external service in case something goes wrong.
class Foo extends Component {
state = { error: false };
componentDidCatch(err) {
logErrorToBackend(err);
this.setState({ error: true });
}
render() {
// If an error happens somewhere down the tree
// we display a nice error message.
if (this.state.error) {
return <div class="error">Something went wrong...</div>;
}
return <Bar />;
}
}
Because setting a fallback content in an error case is so common we made this even nicer by adding for getDerivedStateFromError
which calls setState
automatically under the hood.
class Foo extends Component {
state = { error: false };
static getDerivedStateFromError(err) {
// return argument for setState
return { error: true }
}
render() {
// If an error happens somewhere down the tree
// we display a nice error message.
if (this.state.error) {
return <div class="error">Whoops! This should not have happened</div>;
}
return <Bar />;
}
}
Hooks
Preact now supports Hooks. React has some phenomenal documentation on hooks that's worth a read, particularly if you're getting started with them for the first time. In Preact, you import hooks from preact/hooks
. If you're using a recent version of most bundlers, any hook functions you don't use won't be included in your application.
Here's what hooks look like in Preact:
import { h, render } from 'preact';
import { useState } from 'preact/hooks';
function Counter() {
const [count, setCount] = useState(0);
// ^ default state value
return (
<div class="counter">
Current count: {count}
<button onClick={() => setCount(count + 1}}> +1 </button>
<button onClick={() => setCount(count - 1}}> -1 </button>
</div>
);
}
render(<Counter />, document.body);
createContext ✅
The createContext
-API is a true successor for getChildContext()
. Whereas getChildContext
is fine when you're absolutely sure to never change a value, it falls apart as soon as a component in-between the provider and consumer blocks an update via shouldComponentUpdate
when it returns false
. With the new context API this problem is now a thing of the past. It is a true pub/sub
solution to deliver updates deep down the tree.
import { createContext } from "preact";
const Theme = createContext("red");
function Button() {
return <Theme.Consumer>
{value => <button style={{ color: value }}>click</button>}
</Theme.Consumer>;
}
function App() {
return <Theme.Provider value="blue">
<Button />
</Theme.Provider>;
}
CSS Custom Properties ✅
Sometimes it's the little things that make a huge difference. With the recent advancements in CSS you can leverage variables for styling:
function Foo(props) {
return <div style={{ "--theme-color": "blue" }}>{props.children}</div>;
}
Devtools Adapter
To be able to support all the recent advancements in the excellent react-devtools
extension we knew we had to redo our devtools adapter. In previous version we disguised Preact as React v15 to connect to them but this was getting more and more difficult with features added in later releases like the Profiler tab.
For Preact X we rewrote our adapter from scratch and can directly hook into our own renderer. This is a lot more straightforward for us and eases feature development greatly. It didn't take long for us to bring the Profiler into Preact 👍
Compat lives now in core
Although we were always keen on adding new features and pushing Preact
forward, the preact-compat
package didn't receive as much love. Up until now
it has lived in a separate repository making it harder to introduce breaking
changes.
// Preact 8.x
import React from "preact-compat";
// Preact X
import React from "preact/compat";
New compat features
forwardRef
UNSTABLE_*
-Lifecycle hooksmemo
Removing old React APIs
To make maintenance easier we dropped all legacy APIs that were available
in React versions prior to v16. This includes the DOM-Factories API,
createClass
, string refs and a few more.
Breaking Changes
We were very careful to introduce as few breaking changes as possible. As a user
the most noticable change will be that props.children
is not guaranteed to be
an array anymore. This change was necessary to be able to support rendering
components that return an array of children without wrapping them in a
root node. On top of that this fixes quite a few issues with third-party
components that expect props.children
to be undefined
when no children are
passed around.
The VNode
shape has changed
We renamed/moved the following properties:
attributes
->props
nodeName
->type
children
->props.children
The children of a VNode are no longer guaranteed to be a flat array.
props.children
could be undefined
or it could be a nested array of children.
Pass props.children
to the newly exported helper toChildArray
to always get
an array back.
import { h, toChildArray } from "preact";
function MyComponent(props) {
// Always convert props.children to an array
const children = toChildArray(props.children);
return <div>I have {children.length} child nodes</div>;
}
Note: toChildArray
will flatten and remove non-renderables like null
, undefined
, true
, and false
from the children array.
setState
no longer modifies state synchronously
In Preact X the state of a component will no longer be mutated synchronously.
This means that reading from this.state
right after a setState
call will
return the previous values. Instead you should use a callback function to
modify state that depends on the previous values.
this.state = { counter: 0 };
// Preact 8.x
this.setState({ counter: this.state.counter++ });
// Preact X
this.setState(prevState => {
// Alternatively return `null` here to abort the state update
return { counter: prevState.counter++ };
});
render()
has changed
The root render
function (the one you import from preact
) has changed. It no
longer returns the newly created DOM element. Its return type is now void
. We
made it simpler, by removing the third argument that was used to hydrate the
DOM. Use the hydrate
function instead to hydrate a server rendered DOM tree.
When render
is called multiple times on the same elements, the trees will be
merged together. It no longer just appends to the DOM. This change will be
very welcomed by new users as it was a frequeuent source confusion 🎉
import { render, hydrate } from "preact";
const root = document.getElementById("root");
// Render into the DOM
render(<div>foo</div>, root);
// calling `render` a second time will merge the trees like you would expect it to
render(<div>foo<span>bar</span></div>, root);
// Or use `hydrate` if you're making use of server side rendering
hydrate(<div>foo</div>, root);
preact/devtools
is part of preact/debug
Our devtools adapter has been moved into our debug
package and doesn't require a dedicated import statement anymore. The preact/debug
package must be imported before preact
to prevent our devtools integration to be overwritten by the default one supplied by the react-devtools
extension.
Better support for Tree-Shaking
A lot has changed in the past years in the bundling space. Most bundlers now
offer excellent support for tree-shaking, where unused exports
can be dropped
if they are not used. In the past we always exported an additional object as
the default
export. But because of the nature of JavaScript it is very hard
to prove that an object
property will never be used, they were never removed.
By removing the default
export completely, only the code you need will
be included in the bunlde. The rest will be tree-shaken away.
// Preact 8.x
import preact from "preact";
// Preact X
import * as preact from "preact";
// Preferred: Named exports (works in 8.x and Preact X)
import { h, Component } from "preact";
Note: This change doesn't affect preact/compat
. It still has both named and a default export to remain compatible with react.
Other breaking changes
- Falsy attributes values are no longer removed from the DOM. For some
attributes (e.g.spellcheck
) the valuesfalse
and''
have different
meaning so being able to renderfalse
is important - The
Component
constructor no longer initializes state to an empty object. If
state has not been previously set, it will be set to an empty object the first
time the component is rendered, after the constructor has been called - A falsy argument passed to
setState
will not enqueue an update. This change was made to support returningnull
from theupdater
function, which allows users to abort an update. If you just want to trigger an update and don't care about the state itself you can call it with an empty objectsetState({})
. - The toplevel
rerender
export has been removed.
Minor Changes
render(null, container)
no longer renders an empty text node but instead renders nothing- We longer support a synchronous
options.debounceRendering
. The value ofoptions.debounceRendering
must asynchronously invoke the passed in callback. It is important that contributors to Preact can consistently reason about what calls tosetState
, etc. do, and when their effects will be applied. See the links below for some
further reading on designing asynchronous APIs.
Known Issues
- Nested
Fragments
lead to more DOM operations than necessary - Hooks state not visible inside the
devtools
panel