How to use Serverless Functions with SSR
In this post Iāll explain how to abstract and reuse the same function logic across all sides of the Gatsby stack; Serverless Functions, Server-side Rendering (SSR) and Static Site Generation (SSG).
Iāve created a demo which demonstrates what can be achieved using this approach. The repository is Open-source and contains x2 branches.
The main branch contains all the code used in the demo site and thereās a minimal example branch with just the bits that make shared functions possible.
Gatsby Shared Functions
- š Gatsby Cloud Preview
- āļø Repository
How Not To Do It
When I first needed to use function logic in both a Serverless Function and an SSR page, I did something like this.
// src/api/some-endpoint.js
export default function handler(req, res) {
res.status(200).json({
message: 'A ok!',
data: [
{ date: '24/09/2022', value: '122' },
{ date: '25/09/2022', value: '117' },
{ date: '26/09/2022', value: '52' },
],
});
}
// src/pages/index.js
import React, { useState, useEffect } from 'react';
const Page = ({ serverData }) => {
const [clientResults, setClientResults] = useState();
const getClientData = async () => {
const response = await fetch('/api/some-endpoint');
const results = await response.json();
setClientResults(results.data);
};
useEffect(() => {
getClientData();
}, []);
return (
<div>
<pre>{JSON.stringify(clientResults, null, 2)}</pre>
<pre>{JSON.stringify(serverData.serverResults, null, 2)}</pre>
</div>
);
};
export async function getServerData() {
const response = await fetch('/api/some-endpoint');
const results = await response.json();
return {
props: {
serverResults: results.data,
},
};
}
export default Page;
This Wonāt Work!
The reason, this wonāt work is because the Serverless Function lives on the server, and getServerData
also lives on
the server. In short this approach is attempting to call a server side function from the server.
Let me explainā¦
getServerData
is run on the server so it canāt really make an HTTP request to the Serverless Function because the
Serverless Function is also on the server. To quote Dustin Schau VP of Engineering at
Gatsby.
It creates a kind of back-and-forth that could introduce undue latency
How To Do It
In a word, abstraction. Since both the Serverless Function and getServerData
live on the server they can import
(require), and use the same function.
Shared Function
You can save a shared function anywhere in your Gatsby project. Iāve chosen to save the file in src/utils
. Below is
just a simple function that returns an array of dates and values. In the demo site Iām making a request to the Google
Analytics API and returning actual page view data for paulie.dev.
Your data may well be different, but the principle is the same.
// src/utils/shared-function.js
module.exports = function () {
return {
data: [
{ date: '24/09/2022', value: '122' },
{ date: '25/09/2022', value: '117' },
{ date: '26/09/2022', value: '52' },
],
};
};
For reference, hereās the shared-function used in the demo site.
Serverless
This time around the Serverless Function imports / requires the shared function and awaits the response before returning it.
// src/api/some-endpoint
const util = require('../utils/shared-function');
export default async function handler(req, res) {
const response = await util();
res.status(200).json({ message: 'A ok!', data: response.data });
}
SSR
Itās a similar story for the getServerData
function. Import / require the shared function and await the response
before returning it to the page.
// src/pages/index.js
export async function getServerData() {
const util = require('../utils/shared-function');
const response = await util();
return {
props: {
serverResults: response.data,
},
};
}
This Will Work!
This will work because both the Serverless Function and the getServerData
function are responsible for their own
ārequestā. Each of their responseās is returned in a way thatās suitable for their application. When successful, the
data can be accessed by the page by either referencing a prop
, or a useState
value.
The main difference with this approach is, itās DRY. The actual ābusiness logicā of what the shared function is doing is reused by both methods. Any changes made to the shared function will be reflected in both the Serverless Function and SSR response.
SSG Page
Yep, as it always has been with Gatsby you can source data from any CMS, API or Database and store the response in Gatsbyās Data Layer ready to be queried using GraphQL.
sourceNodes
For SSG you can import / require the shared function, await the response and then pump the data into Gatsbyās Data Layer
using createNode
.
// gatsby-node.js
const util = require('./src/utils/shared-function');
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
const response = await util();
response.data.forEach((item) => {
actions.createNode({
...item,
id: createNodeId(item.date),
internal: {
type: 'staticResults',
contentDigest: createContentDigest(item),
},
});
});
};
All Together
Back to src/pages/index.js
where itās now possible to query this data using GraphQL and return it to the page using
the data
prop.
// src/pages/index.js
import React, { useState, useEffect } from 'react';
+ import { graphql } from 'gatsby';
+ const Page = ({ data, serverData }) => {
- const Page = ({ serverData }) => {
const [clientResults, setClientResults] = useState();
const getClientData = async () => {
const response = await fetch('/api/some-endpoint');
const results = await response.json();
setClientResults(results.data);
};
useEffect(() => {
getClientData();
}, []);
return (
<div>
<pre>{JSON.stringify(clientResults, null, 2)}</pre>
<pre>{JSON.stringify(serverData.serverResults, null, 2)}</pre>
+ <pre>{JSON.stringify(data.allStaticQuery.nodes, null, 2)}</pre>
</div>
);
};
+ export const query = graphql`
+ query {
+ allStaticResults {
+ nodes {
+ value
+ date
+ }
+ }
+ }
+`;
export async function getServerData() {
const util = require('../utils/shared-function');
const response = await util();
return {
props: {
serverResults: response.data
}
};
}
export default Page;
The Demo
In the demo site, take a look at the Serverless Analytics chart, give the page a refresh. Notice itās never blank.
This is because I first render the chart with data from serverData
. However, because I wanted to make this chart
interactive, I need to make a client side request to the Serverless Function which returns data specific to date ranges
set by the user.
Alsoā¦ go ahead and disable JavaScript in your browser. Notice again, the charts are never blank!
These are āhand crankedā created using nothing more than SVG / SVG elements and good olā mathematics. You can see the
src
for the Line Chart here:
line-chart.js.
I chose to āroll my own chartsā because:
- I like Data Viz.
- Some charting libraries only render the chart in the browser when JavaScript is enabled.
Since the data is available on both the client and the server, I want to make sure the charts can also be rendered on the Server. Server-side rendering SVGās means they are statically generated as HTML elements and donāt require JavaScript to render.
The only slight snag is the tooltip. This doesnāt work if JavaScript is disabled. The tooltip displays more information about each of the plot marks which are kinda important. To overcome this I added a toggle switch which uses the checkbox hack to toggle dates and values in the ticks at the bottom of the chart.
Iāll be writing a little more about SVG charts soon so stay tuned!
Further Reading
- Create an SVG Doughnut Chart from scratch for your Gatsby blog
- Add data to Gatsbyās GraphQL layer using sourceNodes
- Become a Data Champion with Gatsby