Tutorial

Build a SSR App With Preact, Unistore, and Preact Router

Published on December 12, 2019
Default avatar

By

Build a SSR App With Preact, Unistore, and Preact Router

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.

Introduction

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.

Technologies

In this tutorial, you will use the following technology to build the Server-Side Rendered App:

  • Preact - An alternative to React with the same API. It aims to offer a development experience similar to React, albeit with some features stripped away such as PropTypes and Children.
  • Unistore - A centralized state container with component bindings for React and Preact.
  • Preact Router - Helps to manage route in Preact applications. Provides a <Router /> component that conditionally renders its children when the URL matches their path.
  • Webpack - A bundler that helps to bundle JavaScript files for usage in a browser.

Building a SSR App with Preact

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"]
            }
        }]
    ]
}

Building the Preact App

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.

Building the Node Server

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.

Increment and decrement app

Adding CSS Styling

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:

  1. 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: Stylized application page

Conclusion

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.

Learn more about us


About the authors

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel