By Paul Scanlon

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

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!

Serverless Analytics Chart Screenshot

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:

  1. I like Data Viz.
  2. 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

Hey!

Leave a reaction and let me know how I'm doing.

  • 0
  • 0
  • 0
  • 0
  • 0
Powered byNeon