Preact is a simplified alternative to React that focuses on bundle size. A minified bundle with Preact and a minimal Webpack config ends up being around 10KB. A minimal unbundled React bundle ends up being around 100KB because of react-dom. Because Preact bundles are comparatively tiny, Preact is a great choice for making sure your app feels snappy.
$ ./node_modules/.bin/webpack
Hash: a1fc672a4a8c9742be78
Version: webpack 4.23.1
Time: 1769ms
Built at: 2018-11-01 13:00:14
Asset Size Chunks Chunk Names
preact.js 9.64 KiB 0 [emitted] preact
react.js 109 KiB 1 [emitted] react
Entrypoint preact = preact.js
Entrypoint react = react.js
[2] ./preact.js 33 bytes {0} [built]
[4] ./react.js 71 bytes {1} [built]
+ 8 hidden modules
I used the below Webpack config to get the above result.
module.exports = {
entry: {
preact: './preact.js',
react: './react.js'
},
target: 'web',
output: {
path: `${process.cwd()}/bin`,
filename: '[name].js'
}
};
Vanilla JS has gotten very good these days and you often don't need any framework, but once you need to manage state and user input, a framework makes things much easier. Preact is great because it doesn't add too much bloat to your app bundle and gives you just enough of React to save you from having to manually register event listeners on DOM events. In this article, I'll walk through building a basic form using Preact.
Setting Up Preact
Like React, Preact is much easier with JSX, so you'll need to install Webpack, Babel, and a Babel transform for JSX. You should also install a static HTTP server like serve.
npm install preact babel-loader@8.x @babel/core@7.x @babel/plugin-transform-react-jsx@7.x webpack@4.x webpack-cli@3.x serve@10.x
This may seem like a lot to install for a simple app, but Babel and Webpack don't end up in the end bundle, so your app size will still be small.
First, let's set up a basic index.html
file that will serve as the entry point for the
app.
<html>
<head>
<title>Preact Test</title>
</head>
<body>
<div id="content"></div>
<script type="text/javascript" src="bin/index.js"></script>
</body>
</html>
Next, let's create an index.js
file that will render some basic JSX:
const React = require('preact');
class App extends React.Component {
render() {
return <h1>Hello, World!</h1>;
}
}
React.render(<App></App>, document.querySelector('#content'));
To create the bin/index.js
bundle that index.html
will use, we'll need Webpack:
module.exports = {
entry: {
index: './index.js'
},
target: 'web',
output: {
path: `${process.cwd()}/bin`,
filename: '[name].js'
},
module: {
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader'
}
}]
}
};
And to configure Babel, we'll need a .babelrc
file as well:
{
"plugins": ["@babel/plugin-transform-react-jsx"]
}
To compile, run ./node_modules/.bin/webpack
$ ./node_modules/.bin/webpack
Hash: a4ff19ba7c622174cb97
Version: webpack 4.23.1
Time: 578ms
Built at: 2018-11-01 13:20:32
Asset Size Chunks Chunk Names
index.js 9.85 KiB 0 [emitted] index
Entrypoint index = index.js
[0] ./index.js 288 bytes {0} [built]
+ 1 hidden module
Finally, run serve
using ./node_modules/.bin/serve
and visit http://localhost:5000
. You should see "Hello, World" show up.
A Basic Form
Forms in Preact are much easier with the linkstate module, which was split off from core Preact. The linkstate module is tiny relative to Preact itself, so don't worry about it adding bloat.
npm install linkstate@1.x
Like in React, the standard pattern for a form with controlled inputs in Preact is:
1) Load form data in componentDidMount()
2) Display the inputs with the input value bound to state
and update state
when the user modifies the form
3) Call a save()
function when the user submits the form
In the interest of making this form a bit more realistic, I'll put the form behind a setTimeout()
. When you're building an actual app, you will likely be loading this data via an HTTP request in componentDidMount()
. To avoid having to set up an actual HTTP server, this example will use setTimeout()
instead.
componentDidMount() {
this.setState({ loading: true });
setTimeout(() => {
this.setState({ loading: false, firstName: 'Foo', lastName: 'Bar' });
}, 100);
}
Next, let's take a look at the render()
function. Preact render()
functions
are slightly different than React's in that they take 2 params: props
and state
.
For this example, props
doesn't matter, but state
contains the component state.
render(props, state) {
if (state.loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<div>
<h3>First Name</h3>
// linkstate updates `state.firstName` every time the user types in
// this input
<input type="text" value={state.firstName} onInput={linkstate(this, 'firstName')} />
</div>
<div>
<h3>Last Name</h3>
<input type="text" value={state.lastName} onInput={linkstate(this, 'lastName')} />
</div>
<div>
<br/>
<input type="button" value="Submit" onClick={this.save()} />
</div>
</div>
);
}
Why is linkstate
useful? Take a look at the save()
function below. This is
because JavaScript functions lose context when they're assigned to a value. If
save()
didn't return a function and you just used onClick={this.save}
, this
would be undefined in the save()
function. You would instead need to do this.save.bind(this)
, but bind()
is slow.
save() {
return () => console.log(this.state.firstName, this.state.lastName);
}
A Form Component
In practice, your form will usually not be responsible for loading its own data. You'll likely have a top-level component that loads the data, and then several different components underneath that allow the user to modify different parts of the data.
Having a separate form component is tricky because the top-level component will
pass in the initial state of the data as props
. The form component will then
store any modifications as state
internally.
But, when the user submits the form, you'll send an HTTP request to the server
and then the top-level component will get back new data from the server. The
form component then needs to be able to react to the new props
. The way to
handle this in Preact is the componentWillReceiveProps()
hook.
class Form extends React.Component {
componentDidMount() {
this.setState(Object.assign({}, this.state, this.props));
}
componentWillReceiveProps(nextProps) {
// Will get called when the component is already mounted, but `props`
// changed.
this.setState(Object.assign({}, this.state, nextProps));
}
render(props, state) {
return (
<div>
<div>
<h3>First Name</h3>
<input type="text" value={state.firstName} onInput={linkstate(this, 'firstName')} />
</div>
<div>
<h3>Last Name</h3>
<input type="text" value={state.lastName} onInput={linkstate(this, 'lastName')} />
</div>
<div>
<br/>
// Assume `props` contains a `save()` function in the interest of
// not adding something like Redux to this relatively simple example.
<input type="button" value="Submit" onClick={() => props.save(state)} />
</div>
</div>
);
}
}
Below is the updated save()
and render()
for the top-level App
component.
The big change here is that the App
component passes the save()
function
to the Form
component as a prop. This is to avoid having to add
Redux or something similar to this simple example.
In practice you probably should just use a Redux store.
save(data) {
console.log(data.firstName, data.lastName);
setTimeout(() => {
this.setState(Object.assign({}, data));
}, 100);
}
render(props, state) {
if (state.loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<Form
firstName={this.state.firstName}
lastName={this.state.lastName}
save={data => this.save(data)} />
</div>
);
}
All in all, the total page weight is 12KB without any gzipping. Not bad!
Moving On
Preact is a great library for when you want to build a simple form without adding 100KB+ to your total page weight. Vanilla JS is great for display-only pages, but starts getting cumbersome once you need to handle some non-trivial user input. Next time you want to use React but feel like it is too heavy, give Preact a shot.