SayKiat / Essays /

Trying Out Server-Side Rendering with Dynamic Pages Using GatsbyJS

Two people looking at a cryptocurrency chart differently. Image by Coingekco.

I created a million-dollar cryptocurrency startup using CoinGecko’s API. Easy.

One of the exciting announcements from GatsbyJS V4 is the introduction of a new rendering option: Server-side Rendering. If you’re more of a static-site person, you would probably be more excited about DSG, especially if you are working on sites that have 1,000+ pages, such as content marketing sites or blogs.

However, given the abundance of third-party APIs and server-side rendering are much of an “out-of-the-box” feature in some React meta-frameworks, I thought to try out GatsbyJS’s SSR and see if it’s user-friendly enough for my next project!

New to GatsbyJS?

If you’re new to the idea of Static Site Generation and Server-Side Rendering, be sure to check out this article: Rendering Options in GatsbyJS. You’ll get a better idea of the cons and pros of using each other and how to use different rendering techniques to complement each other.

About the Project

In this project, I will:

  • explore GatsbyJS’s method of doing SSR by using the function getServerData, which is quite similar to NextJS’s getServerSideProps,
  • consume CoinGecko’s Cryptocurency API to create a listing page that shows all available crypto coins. Clicking each coin will open a single coin page. In other words, we will create dynamic pages using GatsbyJS’s SSR.
  • give my little verdict about GatsbyJS’s SSR.

Before we start, here are a few materials that you can check out:

Project Structure

The project structure is quite standard like any other React/GatsbyJS project, so we will focus only the src folder, starting from the root directory.

/src
/pages
/coin
/[coinId].js ##dynamic pages
/index.js ##listing page
/components
...
/gatsby-config.js
/package.json
...

For simplicity, we will only focus on the bolded files:

  • index.js: Fetches top 100 crypto coins.
  • [coinId].js: Fetches the details of specified crypto coin, e.g. crypto name, image and description.

The index.js

Github

To understand what’s happening on this page, let’s focus on the second half of the code first.

Right after export default IndexPage, we see GatsbyJS’s dedicated SSR function getServerData(), with try/catch in it.

export async function getServerData() {
try {
const res = await fetch(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=false`)
...
  • First, fetch a list of crypto coins from the API URL https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=false
  • Store the fetched response in the constant res
  • Return an object containing props, usually refers to the response.

Then, let’s look at the first half of the code, especially this line:

const IndexPage = ({serverData }) => {
...
##try console.log(serverData)!
}

Just like that, you can access the data as a serverData prop inside your page component. If you console log the serverData, you should able to see an array of coins, which you can map it and do something like this:

{data.map((d,i) => {
return (
<Link key={i} to={`/coin/${d.id}`}>{d.name}</Link>
)
}}

The code above is simplified. Essentially, we tell GatsbyJS to visit the single coin pages at this given path /coin/$\{d.id\}. Meaning, if the id happens to be bitcoin, the link on local will appear as localhost:8000/coin/bitcoin.

Let’s say if we have more than 1,000+ crypto coins, we can dynamically render the single coin pages based on the coin’s id, which we will explore how in the next section.

The [coinId].js

GitHub

We’ll need to create a new file under /src/coin folder. You can name the file name whatever you want, but the name needs to be enclosed with brackets i.e. [filename].js. This tells GatsbyJS that the page will be server-side rendered, and the filename will be dynamic.

Next, let’s focus on the second half of the code again. We see the following:

export async function getServerData(context) {
try {
const res = await fetch(`https://api.coingecko.com/api/v3/coins/$\{context.params.coinId\}`)
...

Notice that this time, we pass the parameter context into the getServerData function to obtain the slug on single coins page. This is done by specifying context.params.whatevergivenfilename. The params object is elaborated in the GatsbyJS SSR documentation.

params: If you use File System Route API the URL path gets passed in as params. For example, if your page is at src/pages/{Product.name}.js the params will be an object like { name: ‘value’ }.Full Reference for Gatsby SSR

Meaning, this ensures that if we are viewing the link /coin/bitcoin, we can obtain the slug bitcoin and pass it to the API URL as https://api.coingecko.com/api/v3/coins/bitcoin. Then, we can return the response as props, and use it on the first half of our code like this:

const CoinPage = ({serverData }) => {
const [data, setData] = useState({})
const [loading, setLoading] = useState(true)
useEffect(() => {
setData(serverData)
setLoading(false)
}, [data])
...

If you console log the serverData, you should be able to see the full API response from CoinGecko.

About Image Optimisation

Unlike NextJS’s Image component, there’s currently no way to optimise API response images on demand.

Paul from GatsbyJS do suggest one workaround, loosely quoting from his tweet reply:

Use a Serverless function, and run Sharp to create the image formats. Calling from a useEffect should be intial page load is still fast

Paul is also kind enough to help implement JIMP on the API response images. You can find the code on the same repo -> [feature/use-jimp-on-images](https://github.com/fsk1997/gatsby-starter-coingecko-ssr/tree/feature/use-jimp-on-images) branch.

Or specifically, replace the following on the ./src/pages/coin/[coinId].js -> getServerData function.

export async function getServerData(context) {
try {
const res = await fetch(
`https://api.coingecko.com/api/v3/coins/${context.params.coinId}`
)
const data = await res.json()
let image = ""
await Jimp.read({ url: data.image.large }).then(img => {
img
.resize(50, 50)
.quality(60)
.greyscale()
.getBase64(Jimp.AUTO, (err, img) => {
image = img
})
})
if (!res.ok) {
throw new Error(`Response failed`)
}
return {
props: { data, image },
}
} catch (error) {
return {
status: 500,
headers: {},
props: {},
}
}
}

What the replacement code does is: resize image, convert it to grayscale and encode it as Base64 format by using JIMP. Do note that Base64 wouldn’t be anymore optimised than a JPG, as pointed out by Paul too. Read more about it here: Why “optimizing” your images with Base64 is almost always a bad idea

However, the lack of image optimisation isn’t a deal-breaker for me. I think that image optimisation should be done at the earliest phase possible. Meaning, if you’re sourcing images from a third-party APIs, the response should provide you multiple image sizes (just like CoinGecko API does). I believe that client runtime operation should be reduced to minimum for best performance.

Regardless of your implementation, if you are using normal image tag, remember to specify the width and height attributes to reduce layout shifts!

And thanks Paul for chipping in some ideas!

Verdict

As this is not a complex project, the developer experience has been delightful overall. With more css and js libraries magic, you can definitely create a full-fledge web app with GatsbyJS’s SSR.


There’s no comment section, yet. If you find my essays interesting, reach out to me personally via email or LinkedIn! I’m always open to talking about ideas.