Build and Deploy a Single Page App with React, Vite, and Netlify Functions
Table of Contents
React has emerged back in 2011 at Facebook (now Meta) and has continued evolving rapidly and gaining a significant ecosystem with a wide variety of plugins and UI frameworks on top of it.
This guide will cover in detail the steps to create a working example bookstore single page application using the latest React 18 and run it using the Vite. It also includes details on how to add a custom state management solution and routing using the wouter library.
The present article is the React equivalent of the Vue 3+Vite article which we’ve published recently on this blog. The end result is the same for both (a bookstore single page app which we’ve called Middlemarch). However, this tutorial also contains a backend solution using serverless functions.
Here’s main tasks that we’ll focus on over the course of this tutorial:
- bootstrap the application skeleton with Vite
- manage routes with the wouter library
- create an internal state management tool
- create and run netlify functions
- write React component tests with Nightwatch
- write and run automated end-to-end tests with Nightwatch
- build and deploy the application with Github Actions
This is a large undertaking, but it accurately describes the whole development cycle. The application will be deployed to Netlify. If you're eager to get right down to coding and you'd like to jump into it right away, you can just get the project up and running with:
git clone https://github.com/pineviewlabs/middlemarch-react
cd middlemarch-react
npm install
npm start
Or fork the project at https://github.com/pineviewlabs/middlemarch-react.
Step 1 – bootstrap the Application skeleton with Vite
We will use the create-vite
CLI to bootstrap the basic structure and dependencies.
npm create vite middlemarch -- --template react
Then follow the instructions that appear at the end, e.g.:
Scaffolding project in /home/projects/middlemarch...
Done. Now run:
cd middlemarch
npm install
npm run dev
The folder structure will likely look similar to:
/
├── src/
| ├── App.css
| ├── App.jsx
| ├── favicon.ico
| ├── index.css
| ├── logo.svg
| └── main.jsx
├─── .gitignore
├─── index.html
├─── package.json
└─── vite.config.js
We don't need the App.css and logo.svg, so you may delete them right away. The last thing we need to do is replace default scripts with these:
{
"scripts": {
"vite:start": "vite",
"vite:build": "vite build"
}
}
vite:
prefix to distinguish the front-end build part from the serverless functions. All about it later.Prepare the App.jsx file
At this point we have prepared the development environment. Let's dig into React and update the App.jsx
file to match the following:
import { StrictMode } from "react";
export default () => (
<StrictMode>
<main></main>
</StrictMode>
);
Now run the npm vite:start
script to get the application running at http://localhost:3000
npm run vite:start
At this point you will see an empty page with the <main>
element under the #root
. The next step is to write pages with the actual content to see something in the browser.
Step 2 – Add routes
It's time to create our application's main routes. In React every page is just a separate component. For this application, we'll consider the following components:
- Home – the very first page that will be served at the
/
base URL - Cart – a page that contains the list of products; it is available only to logged-in users
- Sign-in – the login form
- Register – the user sign-up form
For the sake of the simplicity, I will show you a full implementation of the Home page only. But you may always go to the Github repository to see the final source code of the application.
The whole application will have the following markup:
Let's create the Header and Footer components which remain the same across every page:
mkdir src/components
mkdir src/components/Header
mkdir src/components/Footer
touch src/components/Header/index.jsx
touch src/components/Footer/index.jsx
I am using Unix's touch command to create files and mkdir to create directories. You may use your own editor or IDE to accomplish those tasks.
In the Header/index.jsx file, add the following content:
Link
tag here and the wouter
import. It is one of the Navigation components provided by the wouter library and triggers navigation to the URL from the href
attribute. We will cover that library soon.And the Footer/index.jsx:
After that we need to include them into the application entry point:
The page content will be rendered inside the main
element. You may notice that we declared the file extensions for Home and Footer component files. It is useful to distinguish the local files from external dependencies, though it is completely optional and depends on only your preferences.
What is wouter?
Wouter is a minimal router implementation for React and Preact. You already saw the Link
component which is simply the <a>
element under the hood.
Now it's time to introduce the Route
component which will render a declared component if the URL property matches the current location.pathname
value.
Let's modify the App.jsx component again and declare the routes:
After that we have to provide the default markup for each page:
export default () => <div></div>;
The last thing left is the Home page. Edit the src/pages/Home/index.jsx
file and add th the following code:
For now we will omit the search functionality for brevity. You may notice the new useBooks
import – it is a custom hook which will manage the global books' data of the application. Now, that we mention it let's proceed to the state management section.
Step 3 – state management with Context + Hook
There are plenty state management libraries for React that have diverse ideas behind them. Each of them is worth exploring, but in this small application we don't need to add yet another dependency and write complex logic to store our data, which is only an array of objects that hold the available books. React already has the API which we can use to implement it in a concise way.
Implementing the "useBooks" hook
We will use Context for globally storing the data and hooks for delivering that data to components that need it.
mkdir src/cache
touch src/cache/books.jsx
Here are the steps to do that:
1) Create the context – not exported and it is private to this module
2) Create the Provider component – exported by default
This defines the state where we are going to store the data and returns the context Provider with the value and state dispatcher. The last one we need to dispatch state changes, though it won't be exposed directly to the components.
3) Create the hook
The hook uses the context and returns a fresh array and useful methods which will modify the state.
useBooks
hook exports a tuple (an array of two items) where the first item is the books array and second is collection of methods which describe the available ways of interacting with the books array.Fetchin the /api/books
URL errors for now. It will work when we provide the serverless function for it.
In order to use the books state we need to wrap the application with the component according to the Context using rules:
Step 4 – creating serveless functions with Netlify
Serverless functions is a relatively new concept where the idea is to delegate running and maintaining server-side tasks to a cloud-computing service and only focus on writing the code. There are several service providers that offer this feature, including: Netlify, Vercel, Render, Azure, or Cloudflare.
For this example we are going to use Netlify's functions, but you can choose your favourite provider.
Here is the schema of how everything works:
Install the Netlify CLI
First we have to install the Netlify CLI and create the netlify.toml
configuration file:
npm i -D netlify-cli
touch netlify.toml
The Netlify CLI is needed to emulate the FaaS (Function-as-a-Service) environment and test our functions. The CLI is also responsible for running the application.
We are going to adjust Netlify's config file, so it will know how to run the Vite development server:
Adding a lowdb database
Now let's create the mock file with the data.
mkdir data
touch data/db.json
Okay, let's move on to the functions. By default, Netlify will search for them in netlify/functions
directory.
mkdir -p netlify/functions
touch netlify/functions/books.js
Edit the netlify/functions/books.js and add the following content:
We are using the lowdb package to read from the JSON file. Of course, you can use the fs.promises.readFile
directly and read from the file. But this is an example and in a real-world app you will have a database and you will have to use a database driver so this example shows you that it is possible.
Adding user authentication
Serverless functions allows to implement complex behaviour like user authentication. If you check out to the source code of the current application you may see there two pages:
- SignIn/index.jsx
- Register/index.js
And the corresponding functions:
- login.js
- register.js.
To create an account you just need to pass an email and a password. The latter will be hashed on the server and the hash will be saved into the DB. It's a good practice to add a so-called "salt" to the hashing function which should be private.
For that purpose we will create the .env
file that will contain the PASSWORD_SECRET
variable, which we will use as the "salt".
Here's the contents of the .env.example
file. For local development, rename this to .env
and fill these in:
The VITE_AUTH_KEY
environment variable is used for the remember me functionality.
Configure env variables in Netlify
When running in Netlify, use the administration UI to define the environment variables above:
Step 5 – Testing React components with Nightwatch.js
Component testing is a type of UI testing where the component is rendered in isolation, without the rest of app components, for the purpose of verifying its functionality. It’s usually a testing strategy which happens prior the end-to-end testing step, which we’ll elaborate in the next section.
Install Nightwatch.js
We will use Nightwatch for both component and end-to-end testing. The easiest way to add Nightwatch tests to your project is using the create-nightwatch
init tool.
Just run the following command from the project's root folder and follow the steps:
npm init nightwatch@latest
Install the @nightwatch/react plugin
Nightwatch supports testing React component using the @nightwatch/react, which is using internally the vite-plugin-nightwatch Vite plugin.
To install it, run:
npm i -D @nightwatch/react
After you have run the npm init nightwatch
command, you should have a generated nightwatch.conf.js file.
You need to edit it and let Nightwatch know that React component testing plugin is installed:
Write your first component test
There should be a tests directory now. We can add our first component test.
mkdir tests/component
touch tests/component/basic.js
With the following content:
Nightwatch.js uses the same BDD
syntax as Mocha or Jest. You can even use Mocha as a test runner in Nightwatch but for simplicity we aren't going to do that.
Run your first component test
It's time now to run the above test in Chrome:
npx nightwatch tests/component/basic.js --env chrome
You can pass the --headless
argument if you don't want to see the opening browser (not available if using Safari).
npx nightwatch tests/component/basic.js --env chrome --headless
Extend the component test
For now, the test does nothing helpful. Let's extend it a bit:
Here we have created a simple test that checks for the presence of elements and content on the page. We have refactored the component mounting into the before
hook, so we can do the only checks in the it
block.
Mocking network requests
We have to mock the /api/books
network request while testing in order to ensure reliable results from the server-side component.
That's why we have used the browser.mockNetworkResponse method in that Nightwatch provides in order to fake the response, because component tests don't involve the running server.
Step 6 – Adding end-to-end tests with Nightwatch
End-to-End testing (E2E) helps with validating the most important flows in your application. Fortunately, we don't need to install additional tools to implement E2E tests since we already have Nightwatch in our project (see the previous step for installation instructions).
Nightwatch can run tests against all major browsers thanks to its integration with the W3C Webdriver API and Selenium. It also allows you to use distributed cloud testing platforms like BrowserStack, SauceLabs, CrossBrowserTesting, or LambdaTest.
To build and run the application, use the netlify local dev server:
npx netlify dev
or simply:
npm start
Create your first end-to-end test
mkdir tests/e2e
touch tests/e2e/HomePage.js
The test verifies whether the page is displayed and contains the book information that we've seen before.
Run your first end-to-end test
Run the test by using the following command:
npx nightwatch tests/e2e/HomePage.js --env firefox
You can use another browser or several browsers if you want. Just make sure that you have according drivers installed.
Run the test in Firefox, Chrome and Safari in parallel:
npx nightwatch tests/e2e/HomePage.js --env firefox,chrome,safari
Step 7 – add continuous integration with GitHub Actions
We are almost done. The last thing left is adding CI to automatically run the tests against next changes.
What we need to do is to create the main.yaml
file inside the .github/workflows
directory with the following content.
We'll use Xvfb to run the tests in Chrome inside the Github Actions workflow.
And we are done! GitHub Actions will run tests in 3 separate environments on every pull request.
Where to go from here
If you want to dive deeper you can go to the https://github.com/pineviewlabs/middlemarch-react and explore the code or clone and run locally.
You can also visit the https://middlemarch-react.netlify.app and see the result.
Getting support
React and Wouter
Netlify functions
LowDB
Nightwatch.js
For support with everything Nightwatch related, you may visit the following channels:
- GitHub Discussions
- Nightwatch.js chat server on Discord
Yevhen Kapelianovych is a front-end developer based in Ukraine, working at Odessa-based Halo Lab design agency.