What is a SPA?
Single Page Applications (SPAs) are applications that use one webpage for all their features. In the past, websites typically loaded new pages for each new request or link within the domain name.
For example, there would be an HTML page for:
/
/about
/contact
/blog
/blog/1
Individual HTML pages could be found at the respective URL on the server that accurately represented the file structure (i.e all blog posts were in the /blog
folder). Therefore, when the browser needed the new page, it would request the new page from the server.
Navigating from /blog
to /blog/1
would require a new HTML page.
Controlling the DOM
The approach with SPAs is to turn many requests for HTML pages into one request for the index.html
page, CSS, and Javascript. Then, reactive Javascript frameworks take control of the document object model which is a tree-link structure representing the nodes on an HTML webpage.
For example, take a look at this HTML block:
<html> <body> <div> <h1></h1> </div> <div> <article></article> </div> </body> </html>
All of the elements inside an element are considered the children. Therefore, <body>
is a child of <html>
. Subsequently, <html>
is the parent of <body>
.
Javascript frameworks (React, Vue, etc.) control what is rendered to the DOM (in a much more complicated way) by controlling these elements of the tree. Instead of requesting a new tree, they manipulate the branches via the DOM.
Multi-page Behavior
If you have ever used create-react-app then you know there is only one HTML page in the public
folder. Consequently, you might ask how can multiple URLs give the website a multi-page feeling? The answer: client-side routing libraries.
JavaScript libraries (most notably react-router-dom) allow the developer to register URLs—and their corresponding components—in one of the parent nodes of the DOM tree. Then, depending on the URL entered, certain nodes are rendered to the DOM, and others are not.
How Do SPAs Get Data?
SPAs that implement multi-page behavior still need to get data for the different components as components render. SPAs do not stop making HTTP requests after the initial request. They make smaller requests for only the data (typically JSON data) that is needed whereas a traditional website would request the entire HTML document again (as well as the data).
In React, the data fetching is done inside of the component with functions like componentDidMount
or using React hooks.
SPA vs. MPA
There are certainly tradeoffs that happen when deciding whether to create a single page application or a multi-page application.
SPAs can be high performing applications once they have loaded in the client because they can route between components quickly. At the same rate, it could take longer to fetch the initial application due to the large Javascript code that is requested.
One of the biggest advantages/disadvantages cited when discussing SPAs vs. MPAs is search engine optimization.
Search engines have crawlers that index information found on websites. SPAs are dynamically rendered, so the information on the page is not always available when a crawler is trying to access it. If it does not render the HTML in time then the search engine will not be able to store the information. On the other hand, traditional websites have HTML pages that are static and can be crawled effectively when the crawler first finds the page.
You Don’t Have to Choose Between SPAs and MPAs
Consider this example scenario. You have developed an app that utilizes rapidly changing data and you want the public to be able to find it. However, you are aware that if you create a SPA it could hurt the chances that people find your app with search engines.
Gatsby.js
Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps
Gatsby claims to be fast because the React code is transformed into static HTML pages. This can give an application the benefits of React as well as the benefits of having static assets for search engines to index.
Also, it’s recommended for use on reactjs.org:
Gatsby is the best way to create static websites with React. It lets you use React components, but outputs pre-rendered HTML and CSS to guarantee the fastest load time.
You may be thinking that creating a static rendered site would defeat the purpose of our very fast dynamically rendered site and you are correct. However, Gatsby also supports client-side routing and client-side routing is what SPAs do.
The Best of Both Worlds
This means that using Gatsby, we can create static HTML pages for marketing purposes and have a dynamically rendered application for our product. That way we can be assured that the parts of the app that need to be indexed are static HTML and the parts of the app that are designed for fast user interaction are high-speed.
In this article, I hope to walk you through creating a multi-page React app with Gatsby that has static HTML pages combined with an application using client-side routing!
How to Create a Multi-Page React App
Prerequisites
- Node.js installed on your machine
- Understanding of how to open a terminal, or command-line on your machine
- Internet connection
- Basic Understanding of React components and React hooks
- Code editor (I am using Visual Studio Code)
1. Set-Up Application
Open up a new terminal and run the following commands to get the project started:
$ mkdir rapidapi-multi-page-react-app $ cd rapidapi-multi-page-react-app/ $ npm init -y $ npm install --save gatsby react-dom react axios
The commands create a folder, initialize an NPM project, and install the packages we need.
Next, create some Gatsby specific folders and files.
src/pages/index.js
src/pages/app.js
src/pages/marketing.js
src/components/
gatsby-node.js
The src
, pages
, and components
folders do not exist, so you will need to create them before adding the .js
files.
Creating individual .js
files in the pages
directory is one of the ways to add static HTML pages to a Gatsby app. Consequently, the app has pages at /app
, /
(index), and /marketing
.
Layout.js
Layout.js
is a common wrapper component in Gatsby that typically contains aspects of the page like the header, footer, and navigation.
Create the file Layout.js
in components
and add the below code to it,
import React from 'react' import { Link } from 'gatsby' const Layout = ({children}) => { return ( <> <nav> <Link to='/'>Home</Link> <Link to='/app'>App</Link> <Link to='/marketing'>Marketing</Link> </nav> <main> {children} </main> </> ) } export default Layout
<Link>
is a Gatsby component, that uses @reach/router, to quickly navigate between views in Gatsby.
Now that we have a layout component we can add components to the static HTML pages.
In index.js
add the code,
import React from 'react'; import Layout from '../components/Layout' const Home = ({location}) => { return ( <Layout> <h1>Home Page <span>{location.pathname}</span></h1> <h2>Static HTML Page</h2> </Layout> ) } export default Home;
Inside of marketing.js
add,
import React from 'react'; import Layout from '../components/Layout' const Marketing = ({location}) => { return ( <Layout> <h1>Marketing Page <span>{location.pathname}</span></h1> <h2>Static HTML Page</h2> <p>Aiming for better SEO</p> </Layout> ) } export default Marketing;
And finally, add the below code to app.js
,
import React from 'react' import Layout from '../components/Layout' const App = ({location}) => { return ( <Layout> <h1>Welcome to the App Page <span>{location.pathname}</span></h1> </Layout> ) } export default App;
In the terminal, execute the command npm install -g gatsby-cli
. This installs Gatsby’s CLI globally.
After it installs, run gatsby develop
in the project’s root folder to start the development server.
This command takes a little bit of time to get warmed up, however, once it’s complete navigate to http://localhost:8000 to view the application.
2. Add Router
index.js
and marketing.js
are complete. When Gatsby builds, the components will be transformed into static HTML pages that load quickly and have better SEO.
However, app.js
is going to hold our dynamically routed application.
First, import Router
and Link
from @reach/router underneath where we import React at the top of the page.
import { Router, Link } from "@reach/router"
The <Router>
component accepts child components that are mapped to specific URLs within the router.
Let’s add a basic information route to demonstrate this.
Add the file MyInfo.js
in the components
folder with the code,
import React from 'react' const MyInfo = (props) => { return ( <> <h2>My Info View</h2> <h3>Client-Only Route</h3> <table> <tr> <th> Name </th> <td> your name </td> </tr> <tr> <th> Email </th> <td> example@demo.com </td> </tr> </table> </> ) } export default MyInfo
Next, in app.js
, add the router and a link to the new page. Below the h1
tag, but still inside of the Layout
component, add:
<nav> <Link to='/app/info'>Info</Link> </nav> <Router basepath="/app"> <MyInfo path="/info" /> </Router>
Notice that the <Router>
component takes a basepath
property. This can help clean up the child routes and, in our case, defines the page to find the client-only routes.
Import MyInfo.js
at the top of app.js
.
import MyInfo from '../components/MyInfo'
Add CSS
Before navigating around our new app, add some CSS to make it a little easier to look at.
In components
add the file layout.css
and insert the styles below,
a { display: block; text-decoration: none; padding: 0.25rem .6rem; color: white; } nav { background: #534B52; list-style-type: none; } td { padding: 5px 10px; text-align: center; } th { padding: 5px 10px; color: white; background-color: black; } table { border-spacing: 5px; } h1 { color: #2C302E; } h2 { color: #474A48; } h3, h4 { color: #909590; } h1,h2,h3,h4 { border-bottom: 2px solid #9AE19D; } span { background: #9AE19D; padding: 1px; margin: 3px; border: 1px solid black; }
Next, import the file at the top of Layout.js
.
import './layout.css
These styles are global and will be applied to all components.
Gatsby-Node
gatsby-node.js
can be an important file in any Gatsby app. Furthermore, it’s another place where pages can be created.
If you tried to navigate to the Info page on the app it would not be found. We need to add ‘page-holders’ to the Gatsby app so that Link
components can find the right views. This is a little quirk with Gatsby, but if this wasn’t done hyperlinks to views would not work.
In gatsby-node.js
add,
// This is called after every page is created. exports.onCreatePage = async ({ page, actions }) => { const { createPage } = actions if (page.path.match(/^\/app/)) { // page.matchPath is a special key that's used for matching pages // with corresponding routes only on the client. page.matchPath = "/app/*" createPage(page) } }
You can read more about the uses of gatsby-node.js
here.
Restart the Gatsby application in the terminal for the page to work properly. Finally, visit the info page when the app restarts. You should see the new client-only route we created.
Great work! You have added your first client-side route!
Default Route Components
Notice that no component is rendered when we first navigate to /app
. Let’s change that by adding a default component as a child of <Router>
.
In components
, create the file Default.js
and add the code below,
import React from 'react' const Default = (props) => { return ( <> <h2>Default App View</h2> <h3>Client-Only Route</h3> </> ) } export default Default
Next, import the component at the top of app.js
and add it inside of the <Router>
component.
<Default path="/"/>
app.js
there is a component that renders as the default. However, we can still navigate to the info component if needed.3. Create Nested Client-Side Routes
Despite our work so far, the client-side routes are lacking the dynamic functionality that we are looking for. Thankfully, <Router>
child components can have children of their own. These are nested routes.
Let’s create a dashboard component at /app/dashboard
that has a default child component.
First, create the file Dashboard.js
in components
and add the code,
import React from 'react' import { Link } from 'gatsby' const DataDashboard = ({children,uri}) => { return ( <div> <h2>Data Dashboard</h2> <h3>Client-Only Route</h3> <nav> <Link to={`${uri}/tsla`}>Data for Tesla</Link> <Link to={`${uri}/aapl`}>Data for Apple</Link> </nav> {children} </div> ) } export default DataDashboard
The two routes that we link to do not exist yet, but notice that they use the uri
prop that is passed to components that add a little DRY programming to the links. In our case, the property has a value of /app/dashboard
.
Next, add the DashboardDefault.js
component to the same folder and insert the code,
import React from 'react' const DashboardDefault = () => { return ( <div> <h2>Nested Dashboard Index</h2> <p>This gets rendered when no data parameters are provided.</p> </div> ) } export default DashboardDefault
Then, import these components at the top of app.js
... import DashboardDefault from '../components/DashboardDefault' import Dashboard from '../components/DataDashboard' ...
Below the <MyInfo />
route add the new route with the nested default and create a link for it in the nav
tag. Here’s a look at the code the component now contains;
import React from 'react' import Layout from '../components/Layout' import { Router, Link } from "@reach/router" import MyInfo from '../components/MyInfo' import Default from '../components/Default' import Dashboard from '../components/Dashboard' import DashboardDefault from '../components/DashboardDefault' const App = ({location}) => { return ( <Layout> <h1>Welcome to the App Page <span>{location.pathname}</span></h1> <nav> <Link to='/app/info'>Info</Link> <Link to='/app/dashboard'>Dashboard</Link> </nav> <Router basepath="/app"> <Default path="/" /> <MyInfo path="/info" /> <Dashboard path="/dashboard"> <DashboardDefault path="/" /> </Dashboard> </Router> </Layout> ) } export default App;
After adding the code above and navigating to the dashboard page, you should see the default dashboard component rendered.
Data Parameters
We can pass data parameters down into the child components of the dashboard in two steps.
- Create child component
- Define the data parameter in the path property
First, create the file Data.js
in components
and add the following component to it:
import React from 'react' const Data = ({ dataId }) => { return ( <> <h2>Data View for {dataId.toUpperCase()}</h2> <h3>Nested Client-Only Route</h3> </> ) } export default Data
Notice, we are expecting a property named dataId
to be available to the component.
Next, import the component into app.js
and add the component with the dataId
parameter specified in the path.
.... <Dashboard path="/dashboard"> <DashboardDefault path="/" /> <Data path=":dataId" /> </Dashboard> ....
Parameters can be extracted from URLs by placing a colon in front of the parameter.
Now when you click on the links ‘Data for Tesla’ and ‘Data for Apple’ the dataId
parameter is available in the Link
component and passed down to the child.
Furthermore, we are showing how to access that property from the child component by extracting it from the props and displaying the ticker symbol in the header.
Clicking between the links, you notice that the URL parameter is changing along with the property.
Well done! URLs can now pass on dynamic data to nested child components. The final task will be doing something useful with the parameter.
However, we are going to fetch the data using RapidAPI.
4. Sign Up For a Free Account on RapidAPI
You will need an account on RapidAPI before subscribing to The Finnhub API. Visit RapidAPI to get signed up if you haven’t already!
5. Subscribe to Finnhub API
If you haven’t guessed, we are going to be fetching stock data, and this can be done with the Finnhub API.
Search for Finnhub API or follow this link to the subscription page. Select the basic subscription.
Notice I have already subscribed to the Basic plan. Therefore, I have a link to Manage and View Usage that takes me to the developer dashboard.
You can track your API usage on the dashboard in case you have concerns about approaching your quota for any of the APIs that you subscribe to.
We have a rate limit of 60 requests per minute, which is plenty.
6. Fetch Stock Data from Finnhub
Using React hooks, let’s fetch stock data and display it in the Data
component.
Add API-Key
WARNING: This method does not secure your API key in production. It is only used to simulate data fetching in an application and hide the API key for public repositories.
Create the files .env.development
and .gitignore
in the root of the project.
In .gitignore
, add the code,
.cache node_modules public .env.* .env
and inside of .env.development
add,
RAPIDAPI_KEY=yourapikey
You can find your API key on the Finnhub dashboard located in the middle section.
Restart the Gatsby application once you have added your key to .env.development
Make API Call
Replace the previous code in Data.js
with the code below:
import React from 'react' import axios from 'axios' const Data = ({ dataId }) => { let [quote, setQuote] = React.useState('') React.useEffect(() => { axios({ "method": "GET", "url": "https://finnhub-realtime-stock-price.p.rapidapi.com/quote", "headers": { "content-type": "application/octet-stream", "x-rapidapi-host": "finnhub-realtime-stock-price.p.rapidapi.com", "x-rapidapi-key": process.env.RAPIDAPI_KEY }, "params": { "symbol": dataId.toUpperCase() } }) .then((response) => { setQuote(response.data) }) .catch((error) => { console.log(error) }) }, [dataId]) return ( <> <h2>Data View for {dataId.toUpperCase()}</h2> <h3>Nested Client-Only Route</h3> {quote && <div> <table> <caption>Quote as of {new Date(Number(quote.t)*1000).toDateString()} for {dataId.toUpperCase()}</caption> <tr> <th>Current</th> <th>High</th> <th>Low</th> <th>Open</th> <th>Previous Close</th> <th>Time</th> </tr> <tr> <td>{quote.c}</td> <td>{quote.h}</td> <td>{quote.l}</td> <td>{quote.o}</td> <td>{quote.pc}</td> <td>{new Date(Number(quote.t)*1000).toLocaleTimeString()}</td> </tr> </table> </div>} </> ) } export default Data
It can look like a lot of code, but not there are not that many things happening in the component.
When the component loads, the API call in React.useEffect
is fired off with the dataId
parameter that it received from its parent.
Then, the response data is set to the state variable quote
.
Finally, when the state variable has data, the data is extracted and rendered to the component.
Notice that switching back-and-forth between the links does not reload the page even though the URL changes. However, new data is fetched from the API! Even with a simple application, you can see the performance benefits.
We can further test our dynamic routes by manually changing the URL.
Change your URL in the browser to http://localhost:8000/app/dashboard/ba
Although this link is not an option in the dashboard, the component receives the new data parameter and fetches the current stock data for Boeing (BA).
Congratulations on creating this multi-page React application!
Conclusion
In this article, we discussed SPAs and MPAs and the tradeoffs between the two approaches. Furthermore, we saw that you don’t need to pick between the two if you chose to use a technology like Gatsby.js to build your application.
Enhanced SEO combined with fast dynamic page routing can be a beneficial set-up for many types of web applications.
I hope you enjoyed creating the application and that what you learned fits into your future applications!
If you have questions about the article or application please leave a comment below!
killshot13 says
This was overall a great article, lots of detail, and I was able to successfully create a Gatsby/React application using this guide. Thank you for taking the time to write this and offer it to the general public at no cost.
That being said, may I respectfully point out an error in the code snippet for the Gastby-Node.js file? I believe it should read as follows.
if (page.path.match(/^\/app/)) {
// page.matchPath is a special key that’s used for matching pages
// with corresponding routes only on the client.
page.matchPath = “/app/*”;
createPage(page);
}
Just wanted to point this out for any beginners out there who might not recognize the syntax here and confuse it with a regex expression. Because if copied and pasted directly the snippet in the article returns the following.
Error: C:\Users\dmreh\Code\rapidapi-multi-page-react-app\gatsby-node.js:6
if (page.path.match(/^/app/)) {
^
SyntaxError: Invalid regular expression flags
It even had me confused at first, because I assumed it was some sort of Gatsby specific expression. But upon adding the backslash, the code compiled perfectly. Many thanks for your attention to this matter!
killshot13 says
Update to the above comment, the error appears to have already been corrected. Feel free to delete my essay above!
Ray Walker says
Gatsby fails to install with NPM. This is on a Linux system. Are your instructions out of date?
rwalk@walkubu:~/git2/rapidapi-multi-page-react-app$ npm install gatsby
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! Found: react@17.0.1
npm ERR! node_modules/react
npm ERR! peer react@”17.0.1″ from react-dom@17.0.1
npm ERR! node_modules/react-dom
npm ERR! react-dom@”^17.0.1″ from the root project
npm ERR! peer react-dom@”^16.4.2 || ^17.0.0″ from gatsby@2.32.4
npm ERR! node_modules/gatsby
npm ERR! gatsby@”*” from the root project
npm ERR! react@”^17.0.1″ from the root project
npm ERR! 1 more (gatsby)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@”15.x || 16.x || 16.4.0-alpha.0911da3″ from @reach/router@1.3.4
npm ERR! node_modules/gatsby/node_modules/@reach/router
npm ERR! @reach/router@”^1.3.4″ from gatsby@2.32.4
npm ERR! node_modules/gatsby
npm ERR! gatsby@”*” from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with –force, or –legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /home/rwalk/.npm/eresolve-report.txt for a full report.
npm ERR! A complete log of this run can be found in:
npm ERR! /home/rwalk/.npm/_logs/2021-02-19T22_19_23_415Z-debug.log
Jarrett Retz says
Hey Ray,
Thanks for leaving a comment. I found an issue with installing Gatsby using NPM v7 on the Gatsby Github page.
https://github.com/gatsbyjs/gatsby/issues/26688
Looks like you have to add the –legacy-peer-deps if using NPM v7. For information on NPM v7 and what’s changed you can check ou the link below to a blog article that details some of those changes.
https://blog.logrocket.com/whats-new-in-npm-v7/
Using the legacy flag will allow you to download Gatsby for the project with NPM v7, but I’m not sure what side effects that may cause (if any). Or, you can install npm v6 with the command npm install -g npm@6 to finish this tutorial.
Switching versions of Node is easy with NVM, but unfortunately it can’t be done with the same ease for NPM.