Single page apps are a popular way of building modern web applications. When it comes to SPAs, there are two ways in which you can render the content of the app to your users: client side rendering or server-side rendering.
With client side rendering, whenever a user opens up the app, a request is sent to load up the layout, HTML, CSS, and JavaScript. In cases where the content of the application is dependent on the completion of successfully loading the JS scripts, this can be a problem. This means users would be forced to view a preloader while waiting for the scripts to finish loading.
Server-Side Rendering operates differently. With SSR, your initial request will first load the page, layout, CSS, JavaScript, and content. SSR makes sure that data is properly initialized at render time. Server-side rendering is also better suited for search engine optimization.
In this tutorial, you are going to explore how to build a server-side rendered app with Preact. preact-router will be used for routing, unistore for state management and Webpack for JS bundling. Some existing knowledge of Preact, Unistore, and Webpack might be needed.
In this tutorial, you will use the following technology to build the Server-Side Rendered App:
<Router />
component that conditionally renders its children when the URL matches their path.The construction of this app will be divided into two sections. You’ll first build the server-side of the code which will be in Node and Express. After that, you will code the Preact part of the code.
The idea is to create a Preact app as it were and hook it up to a Node server using the preact-render-to-string
package. It allows for rendering JSX and Preact components to an HTML string which can then be used in a server. This means we’ll be creating Preact components in a src
folder and then hook it up to the Node server file.
The first thing to do is to create the directory for the project and the different folders you’ll need. Create a folder named preact-unistore-ssr
and run the command npm init --y
inside of the folder. That creates a minimal package.json
and an accompanying package-lock.json
.
Next, install some of the tools you’ll be using for this project. Open up the package.json
file and edit with the code below, then run the npm i
command.
{
"name": "preact-unistore-ssr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-react-jsx": "^6.24.1",
"babel-preset-env": "^1.6.1",
"file-loader": "^1.1.11",
"url-loader": "^1.0.1",
"webpack": "^3.11.0",
"webpack-cli": "^2.0.13"
},
"dependencies": {
"express": "^4.16.2",
"preact": "^8.2.6",
"preact-render-to-string": "^3.7.0",
"preact-router": "^2.6.0",
"unistore": "^3.0.4"
}
}
That will install all the packages needed for this application. In the devDependencies
object, there are some babel packages that will help with transpiling ES6 code. file-loader
and url-loader
are Webpack plugins that help with importing files, assets, modules, and more.
In the dependencies
object, you install packages like Express, Preact, preact-render-to-string, preact-router, and unistore.
Next, create a Webpack config file. Create a file named webpack.config.js
in the root of the project and edit it with the code below:
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname, "dist"),
filename: "app.js"
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
}
]
}
};
In the webpack config above, you defined the entry point to src/index.js
and the output to be dist/app.js
. You also set the rules for using Babel. The entry point file does not exist yet, but you will create it later.
Since you’re using Babel, you need to create a .babelrc
file in the root of the project and put in the config.
//.babelrc
{
"plugins": [
["transform-react-jsx", { "pragma": "h" }]
],
"presets": [
["env", {
"targets": {
"node": "current",
"browsers": ["last 2 versions"]
}
}]
]
}
Next, you’ll begin to create files for the Preact side of things. Create a src
folder and create the following files in it:
store/store.js
About.js
App.js
index.js
router.js
Now you can edit the files with the necessary code. Start with the store.js
file. This will contain the store data and actions.
import createStore from 'unistore'
export let actions = store => ({
increment(state) {
return { count: state.count + 1 }
},
decrement(state) {
return { count: state.count - 1 }
}
})
export default initialState => createStore(initialState)
In the code block above you export a set of actions which increments and decrements the value of the count
by 1. The actions will always receive state
as the first parameter and any other parameters may come next. The createStore
function, which is used to initialize the store in Unistore, is also exported.
Next, edit the router.js
file. This contains the set up for the routes you’ll be using in the app.
import { h } from 'preact'
import Router from 'preact-router'
import { App } from "./App";
import { About } from "./About";
export default () => (
<Router>
<App path="/" />
<About path="/about" />
</Router>
)
This code uses the preact-router
to define routes. To do this, import the routes and make them the children of the Router
component. You can then set a prop
of path
to each component so that preact-router
knows which component to serve for a route.
There are two main routes in the application: the App.js
component, which serves as the home route, and the About.js
component, which serves as the about page.
Next edit the About.js
with the following:
import { h } from "preact";
import { Link } from "preact-router/match";
export const About = () => (
<div>
<p>This is a Preact app being rendered on the server. It uses Unistore for state management and preact-router for routing.</p>
<Link href="/">Home</Link>
</div>
);
This is a component that has a short description and a Link
component that leads to the home route.
App.js
serves as the home route. Open that file and edit with the necessary code:
import { h } from 'preact'
import { Link } from 'preact-router'
import { connect } from 'unistore/preact'
import { actions } from './store/store'
export const App = connect('count', actions)(
({ count, increment, decrement }) => (
<div class="count">
<p>{count}</p>
<button class="increment-btn" onClick={increment}>Increment</button>
<button class="decrement-btn" onClick={decrement}>Decrement</button>
<Link href="/about">About</Link>
</div>
)
)
In this code, the connect
function is imported, as well as the actions
function. In the App
component, the count
state value is exposed as well as the increment
and decrement
actions. The increment
and decrement
actions are both connected to different buttons with the onClick
event handler.
The index.js
file is the entry point for Webpack. It’s going to serve as the parent component for all other components in the Preact app. Open up the file and edit with the code below.
// index.js
import { h, render } from 'preact'
import { Provider } from 'unistore/preact'
import Router from './router'
import createStore from './store/store'
const store = createStore(window.__STATE__)
const app = document.getElementById('app')
render(
<Provider store={store}>
<Router />
</Provider>,
app,
app.lastChild
)
In the code block above, the Provider
component is imported. It’s important to specify the working environment if it’s Preact or React. We also import the Router
component from the router.js
file and the createStore
function is also imported from the store.js
file.
The const store = createStore(window.__STATE__)
line is used to pass the initial state from the server to client since you’re building a SSR app.
Finally, in the render
function, you wrap the Router
component inside the Provider
component to make the store available to all child components.
That completes the client side of things. We’ll now move to the server-side of the app.
Start by creating a server.js
file. This will house the Node app that will be used for the server-side rendering.
// server.js
const express = require("express");
const { h } = require("preact");
const render = require("preact-render-to-string");
import { Provider } from 'unistore/preact'
const { App } = require("./src/App");
const path = require("path");
import Router from './src/router'
import createStore from './src/store/store'
const app = express();
const HTMLShell = (html, state) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
<title> SSR Preact App </title>
</head>
<body>
<div id="app">${html}</div>
<script>window.__STATE__=${JSON.stringify(state).replace(/<|>/g, '')}</script>
<script src="./app.js"></script>
</body>
</html>`
app.use(express.static(path.join(__dirname, "dist")));
app.get('**', (req, res) => {
const store = createStore({ count: 0, todo: [] })
let state = store.getState()
let html = render(
<Provider store={store}>
<Router />
</Provider>
)
res.send(HTMLShell(html, state))
})
app.listen(4000);
Let’s break this down:
const express = require("express");
const { h } = require("preact");
const render = require("preact-render-to-string");
import { Provider } from 'unistore/preact'
const { App } = require("./src/App");
const path = require("path");
import Router from './src/router'
import createStore from './src/store/store'
const app = express();
In the code block above, you import the packages needed for the Node server, such as express
and path
. You also import preact
, the Provider
component from unistore
, and most importantly the preact-render-to-string
package which enables you to do server-side rendering. The routes and store are also imported from their respective files.
const HTMLShell = (html, state) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
<title> SSR Preact App </title>
</head>
<body>
<div id="app">${html}</div>
<script>window.__STATE__=${JSON.stringify(state).replace(/<|>/g, '')}</script>
<script src="./app.js"></script>
</body>
</html>`
In the code block above, you create the base HTML that will be used for the app. In the HTML code, the state is initialized in the script
section. The HTMLShell
function accepts two parameters. The html
parameter will be the output received from preact-render-to-string
, and then html
is injected inside the HTML code. The second parameter is the state.
app.use(express.static(path.join(__dirname, "dist")));
app.get('**', (req, res) => {
const store = createStore({ count: 0})
let state = store.getState()
let html = render(
<Provider store={store}>
<Router />
</Provider>
)
res.send(HTMLShell(html, state))
})
app.listen(4000);
In the first line of code, you tell Express to use the dist
when serving static files. As mentioned earlier, the app.js
is inside the dist
folder.
Next, you set a route for any request that comes into the app with app.get(**)
. This first thing to do is to initialize the store and its state, and then create a variable that holds the value of the state.
After that, preact-render-to-string
(which was imported as render
) is used to render the client side Preact app alongside the Router
, which holds the route, and Provider
, which provides the store to every child component.
With that done, you can finally run the app and see what it looks like. Before you do that, add the code block below to the package.json
file.
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start:client": "webpack -w",
"start:server": "babel-node server.js",
"dev": "npm run start:client & npm run start:server"
},
These are scripts that allows you to get the app up and running. Run the command npm run dev
in your terminal and go to http://localhost:4000
. The app should be up and running and you will get a display similar to the one below.
Now that are views are done and the client is hooked up to the server you can add some styling to the app. You’ll need to let Webpack know that it needs to bundle CSS files.
To do that, style-loader
and css-loader
need to be added to the app. Both can be installed by running this command:
- npm i css-loader style-loader --save-dev
Once the installation is complete, head over to the webpack.config.js
file and add the the code below inside the rules
array.
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
You can now create an index.css
file inside the src
folder and edit with the following code:
body {
background-image: linear-gradient(to right top, #2b0537, #820643, #c4442b, #d69600, #a8eb12);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
a {
display: block;
color: white;
text-decoration: underline;
}
p {
color: white
}
.count p {
color: white;
font-size: 60px;
}
button:focus {
outline: none;
}
.increment-btn {
background-color: #1A2C5D;
border: none;
color: white;
border-radius: 3px;
padding: 10px 20px;
font-size: 14px;
margin: 0 10px;
}
.decrement-btn {
background-color: #BC1B1B;
border: none;
color: white;
border-radius: 3px;
padding: 10px 20px;
font-size: 14px;
margin: 0 10px;
}
In the index.js
file, add this code at the top of the file:
import './index.css';`
...
Your page will now be stylized:
In this tutorial, you’ve created a Server-Side Rendered Preact app and explored the advantages of building server-side rendered apps. You also used Unistore for basic state management and hooked up state from the server to the frontend using window.__STATE__
.
You should now have an idea on how to render a Preact app on the server. To summarize, the idea is to initially render the app on the server first and then render the components on the browser.
The code for this tutorial can be viewed on GitHub.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!