How to setup Client-Server Relaying in Next.js
Get the benefits of server-side rendering and client-side data fetching in a single pattern
Cover photo from Mariam Antadze on Pexels
Displaying fresh data as fast as possible to the user is critical if you want to craft enjoyable websites. And I bet you do. From a company owner standpoint, it also makes the difference between earning a revenue, or not.
See this call-to-action below, displaying the number of customers who preordered the latest fancy product of your e-commerce website. If you have worked in the field of web marketing, you know how impactful these can be.
But components displaying dynamic data, like this customers count, are not easy to implement in Next.js.
In Next, there are two places where you can retrieve data: the server, and the user's browser, aka the client. Picking the right place to fetch data can be difficult. But what if I told you that you don't always have to choose?
This article describes how to implement "client-server relaying", a Next.js architectural pattern that lets you rip off the benefits of both server-side rendering (SEO, reduced TTFB) and client-side rendering (interactivity, real-time updates).
It's not just about e-commerce, Client-Server Relaying is also a valuable pattern for blog upvote systems, chat interfaces, administration dashboards, and other similar use cases.
Prerequisites
This article expects readers to have a minimum familiarity with Next.js and React, or a comparable framework with server and client-side rendering capabilities: Nuxt, Svelte, Qwik, Angular and so on.
First, the client
Let's start with client-side data fetching. If you are a React developer, or using any similar library, you'll think something along these lines:
"Welp, that's the easy part! I know how to fetch data in React from the client. I just need fetch, useState and an effect. I am going to skip to the next section about server-side rendering." - You
We've all been there. But are you sure you are aware of the new React documentation, that has recently been rewritten from scratch?
I'll put the link in the reference section at the bottom, but here are 2 major takeaways:
You should probably use a library. Data-fetching is extra annoying to setup because you need to take care about so many little details: loading state, cancellation, caching, cache invalidation (you know, one of the two hard things in computer science), impact on React perfs...
If you don't use a library, you need to setup proper cleanup logic. The "useData" hook demonstrated in the documentation shows a proper implementation, written by React core team.
In this article, we will pick the first road and rely on SWR, a data fetching library from Vercel.
Here is how you can get some data in SWR. It's actually done in 2 steps, first we define a generic "fetcher" function that triggers the HTTP request. Then we use this fetcher within the useSWR
hook.
/**
* A SWR fetcher is a function
* that takes care of the actual HTTP request
*
* Then SWR handles everything else:
* caching, loading and error states, polling...
*/
async function jsonFetch(url: string) {
const res = await fetch(url, {
headers: {
Accept: "application/json"
}
})
const data = await res.json()
return data
}
Then comes the hook itself:
export function PreorderCta() {
const { data /*, error, isLoading*/ } = useSWR<{ count: number }>(
'/api/preorder/count',
jsonFetch)
return <p>{data.count} users preordered the product</p>
}
The usage is pretty intuitive, you receive your data but also a loading indicator and potentially errors.
It looks pretty simple for now. Let's use a more advanced feature of SWR to immediately understand the benefit of having chosen a library rather than rolling our own logic.
This advanced feature is polling. In SWR, you can use the refreshInterval
parameter to tell the hook to send a new request every few seconds. This way, if another customer preorders the product, all other users will automatically get a fresh value without reloading the whole page.
// app/preorder/client-components.tsx
"use client"
export function PreorderCta() {
const { data /*, error, isLoading*/ } = useSWR<{ count: number }>(
'/api/preorder/count',
jsonFetch,
{
// π the preorder count is refreshed every 2 seconds
refreshInterval: 2000
})
return <p>{data.count} users preordered the product</p>
}
You can observe polling in action by opening the network tab of your favorite browser:
Then, the server
The problem with client-side data fetching is that it is subject to cascading. When the user accesses the page, their browser will download the HTML for the page, which in turns references some JavaScript code, which is interpreted, then it finally triggers the request to your API to get the preorder counts.
A few decades later, your app will get the response, and replace the loader with the actual preorder count.
Dull.
The point of server-side rendering, either at build-time (static) or at request-time (dynamic), is to serve an HTML version of the site with data already displayed.
The user will still have to wait a few hundred milliseconds for the page to become interactive with JavaScript, but at least they can already see some interesting data rather than a bunch of ugly loaders.
In Next.js App Router, server-side data fetching is the default way of getting data. The simplest approach is to make the preorder page an asynchronous React Server Component, and fetch the data from there.
It sounds terrifying when written this way, but take a look at the code, it's actually quite short:
// app/preorder/page.tsx
import { cache } from "react"
// I suppose you have a function that can get
// preorders from your database or a 3rd party API
// This is the same function you used to implement the
// "api/preorder/count" API endpoint
import { countPreorders } from "your-db"
// Don't forget to cache for deduplication
// within the React Server Components tree
const rscCountPreorders = cache(countPreorders)
export default async function PreorderPage() {
const preordersCount = await rscCountPreorders()
return <PreorderCta initialCount={preordersCount} />
}
Our PreorderCta
now receives a preorder count directly from its parent, which is a React Server Component. This value can be displayed before the JavaScript code for this page is even downloaded.
Finally, the relay
We have two preorder counts, one fetched from the server, and one fetched client-side. Both have pros and cons.
The server-side value is immediately available, but can only be updated by refreshing the whole page.
The client-side value can be updated automatically every 2 seconds, but it's only available after the React app becomes interactive, a process named hydration.
Pro-tip to reduce the load on your database for the server count
The idea of a Client-Server Relay is to use the server-rendered value as the initial displayed value, and then replace it by the client value when it's ready. The schema below illustrates this pattern in Next.js:
Fortunately, SWR has a super intuitive option that lets us reconcile the server and the client worlds and set up Client-Server Relaying: fallback
.
// app/preorder/client-components.tsx
"use client"
export function PreorderCta({initialCount}) {
const { data /*, error, isLoading*/ } = useSWR<{ count: number }>(
'/api/preorder/count',
jsonFetch,
{
// π the preorder count is refreshed every 2 seconds
refreshInterval: 2000,
// π the server value
// is immediately displayed on the first render
// it is then updated client-side
// when JavaScript is ready (after React hydration)
fallback: { count: initialCount }
})
return <p>{data.count} users preordered the product</p>
}
Conclusion: Next.js is all about hybridization
Client-Server Relaying is a pattern that hybridizes client and server data fetching and rendering for maximum performances. But that's not the only hybrid pattern in Next.js. For instance, Partial Prerendering (PPR) is an experimental feature that let's you mix static and dynamic server-side rendering.
Next.js provides so many different approaches to achieve what seems, at first sight, to be the same thing, exactly for this reason: combining different techniques lets you craft super efficient architectures, that enjoy the benefits of each technique without suffering from their limitations.
Resources
https://swr.vercel.app/docs/revalidation
https://swr.vercel.app/docs/with-nextjs#pre-rendering-with-default-data
https://react.dev/learn/you-might-not-need-an-effect
https://react.dev/reference/react/useEffect#fetching-data-with-effects