Build a server-rendered Preact app from scratch
In this tutorial, we’re going to build a simple server-rendered Preact app, with client-side hydration. We’ll start by serving plain HTML with Express, then build up some components using Preact and HTM. After that, we’ll add some interactivity by hydrating our components, matching up the server rendered HTML with what the client-side framework thinks it should be. Along the way we’ll have some fun (?) with build tools like Nodemon, Rollup, and NPM scripts.
None of the individual parts of this are (relatively speaking) that complex, but there are a lot of moving parts here and it can be hard to find an example that puts them all together. This is an intermediate level tutorial though, so I won’t be explaining much of the syntax or JavaScript language features. You’ll need to be reasonably comfortable with the terminal, NPM and JavaScript basics.
Why server rendered? Why Preact?
Basically, it’s fast. Sending as much as you can via HTML means less work to do on the client to make it work; using a lightweight framework like Preact means you’re sending less JavaScript on the wire, which translates to a faster user experience, especially on slower devices (i.e. most Android phones - start with this article by Alex Russell if you want to delve into the numbers). For content-heavy sites, it’s almost invariably better to get as much of your content onscreen in HTML as quickly as possible. A lot of JavaScript framework SSR approaches involve building a single-page app then hacking in server rendering, but we’re going to start with plain HTML and try to progressively enhance it.
It’s also a good exercise in seeing what’s going on under the hood — in my day job I work on a moderately complex React/Next.js application, and there’s a lot of magic that goes on under the surface. Understanding what your tools are doing for you is always useful.
Table of contents
- An express app that serves HTML
- Setting up Nodemon and NPM scripts
- Setting up Rollup
- Using Preact to render to HTML
- Components and Composition
- Keeping hydrated
- Making it interactive
An Express app that serves HTML
Let’s start with a minimum viable Express server. Create a new folder and switch into it, then initialise NPM - the default options are fine.
$ mkdir preact-ssr
$ cd preact-ssr
$ npm init
If you’re using git for version control, you probably want to add the
node_modules
folder to your .gitignore
file at this point.
Now we’ll install some dependencies we need to get started.
$ npm install express compression
Strictly speaking we don’t need compression, but one of reason we’ve picked Preact and server-rendering is to squeeze out as much performance as possible, so let’s roll with the best practice here and save a few bytes at basically no cost to us.
Create a src
folder, with a server.js
file inside it.
$ mkdir src
$ touch src/server.js
Let’s set up our basic app in src/server.js
to serve some static
HTML.
const express = require('express')
const compression = require('compression')
const app = express() // create the express app
app.use(compression()) // use gzip for all requests
// some basic html to show
const layout = `
<!DOCTYPE html>
<html>
<body>
<h1>Hello</h1>
</body>
</html>
`
app.get('/', (request, response) => {
// listen for requests to the root path
response.send(layout) // send the HTML string
})
app.listen(3000) // listen for requests on port 3000
Run the app:
$ node src/server.js
Then visit http://localhost:3000. All being well, you should see the HTML rendered out. Check out the demo repo at this point if you need to check what it should look like.
There is a slight issue here. Try changing the text in the <h1>
tag
and refresh your page. You should see that it doesn’t update until you
kill your server (using ctrl + c in your terminal) and restart it. That
will get annoying fast, so let’s take the time now to fix our workflow.
Setting up Nodemon and NPM scripts
Nodemon is a tool that listens for changes to specified files and restarts the Node process when it sees one. Let’s add it as a development dependency.
$ npm install nodemon --save-dev
We’ll add an NPM script so we don’t have to remember the right
incantation every time. You can do this all via the CLI if you like, but
let’s automate as much as we can. In the "scripts"
block of your
package.json
, let’s add a new entry.
"scripts": {
"nodemon": "nodemon --watch src/server.js src/server.js"
}
This translates roughly to “run node src/server.js
, but also watch
src/server.js
and rerun the command whenever you see that file has
changed”.
You should now be able to start the app using npm run nodemon
, change
some text in the <h1>
and refresh to see it in the browser
immediately. All being well, your app should look something like the
demo at this commit.
Setting up Rollup
So far, we’ve been using Node’s require
syntax to load dependencies,
but we’re going to want to add packages that are designed to use ES6’s
import
. Let’s use a bundler to paper over the differences between
them so we can avoid gazing too deeply into that particular abyss.
Webpack is a popular bundler, but configuring it is baffling at the best
of times, so we’ll go with Rollup. We don’t need much, just the
ability to use both require
and import
as needed, on both the server
and the client. How hard can it be, right?
First up, we’ll install rollup
, as well as the node-resolve
plugin.
npm install --save-dev rollup @rollup/plugin-node-resolve
Next, let’s tell Rollup to take our src/server.js
file and compile it
into build/server.js
. Create a rollup.config.js
file at the root of
your project. Note that we’re exporting an array of config objects, as
we’ll be adding a client bundle soon. I’d also recommend adding the
build
folder to your .gitignore
file, although it’s not mandatory -
some people prefer to check in the built files.
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
export default [
{
input: 'src/server.js', // take our source file
output: {
file: 'build/server.js', // compile it into this file
format: 'cjs', // use the CommonJS format, which works with Node
},
plugins: [resolve()], // use the node-resolve plugin so dependencies get imported properly
},
]
Add another script to the package.json
for Rollup. This tells Rollup
to use the config file we just created, watch for changes to imported
files and recompile when it sees a change.
"rollup": "rollup --config --watch",
Update the Nodemon script to use the built file, so it will restart the server every time Rollup finishes compiling.
"nodemon": "nodemon --watch build/server.js build/server.js"
Now we have two NPM scripts, rollup
and nodemon
, that we want to run
in the background. At first glance, you might want to try $ npm run rollup && npm run nodemon
; that won’t work though, as the rollup
watcher never exits, so the nodemon
script never starts. We need a way
to run them in parallel. You could do this by hand, but fortunately,
there’s a package for doing it in a single script. Let’s install it.
$ npm install --save-dev npm-run-all
Then we’ll add a start
script that uses it to call both of the
scripts we prepared earlier, in parallel.
"start": "npm-run-all --parallel nodemon rollup"
Now you can start both processes with a single command.
$ npm run start
There were quite a lot of moving parts in there, so compare against the demo repo at this point if something’s not working.
If you have a look at your build/server.js
file, it should look pretty
much like src/server.js
, but now we’re all set up for importing ES6
modules, so let’s do that.
Using Preact to render to HTML
Time to install some more dependencies. We’ll install Preact itself,
the Preact server-side renderer, and htm
, which lets us use
JavaScript’s tagged template strings to build up our components (you
can also use JSX, but that requires an additional compile step in
your build process). If you’ve used JSX with React or another framework
before, you’ll probably have to be careful your muscle memory doesn’t
take over here; the syntax isn’t that complicated, but it feels more
like EJS (or ERB, for the Rubyists) than JSX sometimes.
$ npm install preact preact-render-to-string htm
In your src/server.js
file, import what we need at the top of the
file.
import render from 'preact-render-to-string'
import { html } from 'htm/preact' // use the provided preact binding
Generate some markup and render it to a string using the functions we
imported from preact-render-to-string
and htm
.
const body = render(html`<h1>Hello from Preact</h1>`)
Then interpolate that variable into our final HTML document.
const layout = `
<!DOCTYPE html>
<html>
<body>
${body}
</body>
</html>
`
Refresh your browser and you should see the text from your body
variable, except now we’ve rendered it using Preact before turning it
into HTML. Have a look at the repo at this point if not.
Components and Composition
One of the reasons Preact, React and other JavaScript frameworks are
popular is because they make it easy to create small, separate
components, then compose those components together to create larger
applications. Let’s refactor our single server.js
file into
components. We’ll then render the whole component tree on the server,
before adding the client-side JavaScript to make it interactive.
Create a src/components
folder, and add List.js
and PreactApp.js
files to it.
$ mkdir src/components
$ touch src/components/List.js
$ touch src/components/PreactApp.js
90% of web development is looping over lists of things, so in
src/components/List.js
, we’ll create a component that takes an array
of data and renders it in a <ul>
.
import { html } from 'htm/preact'
const List = ({ data }) => {
// takes a data prop
return html`
<ul>
<!-- loop over data array -->
${data.map(
(i) => html`
<li>
<!-- render out each item -->
${i}
</li>
`
)}
</ul>
`
}
export default List
In src/components/PreactApp.js
, we’ll import List.js
and pass it
some data.
import { html } from 'htm/preact'
import List from './List'
const dataArray = ['Item one', 'Item two', 'Item three']
const PreactApp = () => {
return html` <${List} data=${dataArray} /> `
}
export default PreactApp
Then in src/server.js
, we’ll import src/components/PreactApp.js
and
render that to a string, instead of just writing it inline.
import PreactApp from './components/PreactApp'
...
const body = render(html`
<h1>Hello from Preact</h1>
<${PreactApp} />
`)
When you refresh, you should see the <h1>
, followed by the list of
data from our PreactApp
and List
components. Compare against the
repo at this point if required.
Next up, let’s add some interactivity to our List
component.
Keeping hydrated
Hydration is the process of reconciling the server-rendered DOM structure with what our client-side app thinks should be happening. When a framework like Preact sets up an app, it works out what it thinks the DOM should look like and works out the most efficient set of changes to get it into that state, then adds things like event listeners to the right elements. When we use hydration, we’re saying to Preact “Don’t worry about working out what changes are required, the DOM’s already in the right state. Just add your event listeners and carry on.”
Before we can do that, we need to load some client-side JavaScript, as
currently we’re just sending HTML with no <script>
tags in sight. We
have to do a few things:
- Create a source file for our client bundle
- Configure Rollup to compile it into the right format for the browser
- Configure Express to serve it as a static file
- Add a
<script>
tag into the HTML Express is serving - Add some interactive elements into our app!
Start by creating the source file.
$ touch src/client.js
In src/client.js
, import the PreactApp
component, and tell Preact
where in the DOM we want to consider as our app.
import { hydrate } from 'preact'
import PreactApp from './components/PreactApp'
hydrate(PreactApp(), document.getElementById('root'))
In src/server.js
, add a <div>
with a matching ID around the
PreactApp
component.
<div id="root">
<${PreactApp} />
</div>
In rollup.config.js
, add a second config object into the array. This
has different settings from the server.js
file, as the browser
doesn’t understand Node’s CommonJS syntax natively.
import resolve from '@rollup/plugin-node-resolve'
export default [
{
input: 'src/server.js',
output: {
file: 'build/server.js',
format: 'cjs', // CommonJS format for Node
},
plugins: [resolve()],
},
{
input: 'src/client.js',
output: {
file: 'build/client.js',
format: 'es', // ES Module format for modern browsers
name: 'client',
},
plugins: [resolve()],
},
]
Now when you run npm run start
, you should see a client.js
popping
into the build
folder alongside server.js
.
Next, let’s tell Express how to serve our client bundle as a static file.
app.get('/client.js', (request, response) => {
response.sendFile('client.js', {
root: __dirname, // this will be the build folder
})
})
Now you can visit http://localhost:3000/client.js and see some compiled
JavaScript. Let’s pop that in a script tag in src/server.js
.
const layout = `
<!DOCTYPE html>
<html>
<body>
${body}
<script type="module" src="client.js" async></script>
</body>
</html>
`
Now when you refresh http://localhost:3000, you have a working, hydrated Preact application that does… precisely nothing (if you don’t, have a look at the example repo at this stage). Let’s fix that.
Making it interactive
All we need to do now is add some functionality into our
src/components/List.js
component. On the server it gets compiled to
HTML, while our client bundle will look out for the matching DOM
elements and set up the required listeners when it hydrates. We’ll add
a button to each list item, another item to show how many times they
were clicked, and we’ll use Preact’s useState
hook to track that
data.
import { html } from 'htm/preact'
import { useState } from 'preact/hooks'
const List = ({ data }) => {
// takes a data prop
// how many clicks have we counted? Default to 0
const [count, setCount] = useState(0)
// shared event handler
const handleClick = () => {
setCount(count + 1)
}
return html`
<ul>
${data &&
data.map(
(i) => html`
<li>
<!-- listen for button clicks -->
${i}: <button onClick=${handleClick}>Click me</button>
</li>
`
)}
<li>
<!-- list how many clicks we've seen, with the right plural -->
${count} ${count === 1 ? 'click' : 'clicks'} counted
</li>
</ul>
`
}
export default List
You should now have a functional Preact application that still does as much of its work up-front on the server as possible. Again, if you’re having issues, try comparing against the example repo. Hopefully that is a useful starting point - there’s still a lot of low-hanging fruit that we could build into this (Things like compiling CSS, minifying our scripts, transpiling JavaScript using Babel, and rendering more complex state), but I’ll save those for a future tutorial.
If you have feedback or suggestions, then hit me up on Twitter or open an issue on the repo.