Display a view counter on your blog with React Server Components
Posted on April 24, 2023
I remember the time it was so cool to have a view counter on my personal website. A perfect beginner project for web developers, just before the more complicated guest book. Now it isn’t as common as it used to be, but I wanted to add such a view counter on my blog, for each post. Here is a short tutorial in case you want to add one on yours, combining React Server Components, Streaming and Suspense in a Next.js application.
Instead of building my own counter storing the data somewhere, I decided to rely on Plausible, the tool I’m using for analytics. Its API offers an easy way to get the view count for a given page, which is exactly what I need.
Here is the function I wrote responsible for getting the view count for any page of my website:
// lib/plausible.ts
export async function getCountForPage(page: string): Promise<number> {
const siteId = 'scastiel.dev'
const period = '12mo'
const filters = encodeURIComponent(`event:page==${page}`)
const params = `site_id=${siteId}&period=${period}&filters=${filters}`
const url = `https://plausible.io/api/v1/stats/aggregate?${params}`
const headers = { Authorization: `Bearer ${process.env.PLAUSIBLE_API_KEY!}` }
const res = await fetch(url, { headers })
return (await res.json()).results.visitors.value
}
Now, where should I call this function?
Classic client-server communication with React
Classicly, this function is called in a serverless function, e.g. /api/count?path=...
, and we can call this serverless function from a React component (or a custom hook). Here is cool way to do it using the great useSwr
hook:
import useSwr from 'swr'
const fetcher = (url: string) => fetch(url).then((res) => res.json())
function ViewCount({ page }: { page: string }) {
const { data } = useSwr(`/${slug}/stats`, fetcher)
if (!data) return null
return <span>{data.count} views</span>
}
This works pretty well. The blog post is rendered by the browser, then an HTTP request is sent to the server, returning the view count, which is then rendered on the page.
This way of doing is the one I’m using on this blog for individual post. What follows in this post can only by used in pages using Server-Side Rendering, not the ones statically generated (and all my post pages are statically generated). The solution I present is used on the Articles page to display views count for each post.
But React Server Components offer an elegant pattern we can use. What if the view count was sent with the request returning the blog post itself?
Using React Server Components and SSR
With Server-Side Rendering (SSR), server components are rendered only on the server, which gives them the ability to perform async operations for instance. We can create a PageViews
component calling the getCountForPage
function, and then rendering the count.
// components/PageViews.tsx
import { getCountForPage } from '@/lib/plausible'
export async function PageViews({ slug }: { slug: string }) {
const count = await getCountForPage(`/${slug}`)
return <span title="Views in the last year">{count} views</span>
}
Now we can use this PageViews
component anywhere, like in the page.tsx file responsible for rendering a blog post:
// app/[slug]/page.tsx
import { PageViews } from '@/components/PageViews.tsx'
export default function PostPage({
params: { slug },
}: {
params: { slug: string }
}) {
// ...
return (
<div>
{/* @ts-expect-error */}
<PageViews slug={slug} />
</div>
)
}
While already supported by React, TypeScript is still a bit relunctant to using async components in JSX (hence the @ts-expect-error
), but it’s coming with TypeScript 5.1 at the time I’m writing this post 😉.
Now you might think that will not be great if fetching the view count from Plausible takes some time (like one second), as it would block the rest of the page from being sent to the browser.
This is where Streaming comes handful!
Leveraging Streaming to send intermediate results
Streaming allows the server to send a first version of the page, keep the connection open, then send a second version (with our view count) a bit later.
And as implementing streaming might sound scary, it turns out React does all the work for us! All we have to do is wrap our <PageViews />
in a <Suspense />
block:
return (
<div>
<Suspense fallback={<span>Loading…</span>}>
{/* @ts-expect-error */}
<PageViews slug={slug} />
</Suspense>
</div>
)
Now, React won’t wait for PageViews
to be rendered. On the server, it will first render a first version using the provided fallback and send it to the browser. Then, when the PageViews
part is ready (i.e. when we got our view count), it will send a second version to the browser.
From the user point of view, this solution is very similar to the first one. But from ours, the code is easier to read and maintain, as we delegate a lot to React and to the server.
We can obviously do much more with React Server Components and Streaming, but I like this tiny use case, as it’s the perfect opportunity to get familiar with these new features.
Do you have any cool use case to share?
Some resources to know more:
- Streaming and Suspense in Next.js’ documentation.
- Server and Client Components in Next.js’ documentation.
- RFC: React Server Components, might not be the easiest way to start with Server Components 😅.
Check my latest articles
- 📄 A better learning path for React with server components (May 26, 2023)What if we took advantage of React Server Components not only to improve how we use React, but also how we help people learn it from the beginning?
- 📄 Using Zod & TypeScript for more than user input validation (March 8, 2023)If you have ever created an API or a form accepting user input, you know what data validation is, and how tedious it can be. Fortunately, libraries can help us, such as Yup or Zod. But recently, I realized that these libraries allow patterns that go much farther than input validation. In this post, I’ll show you why I now use them in most of my TypeScript projects.
- 📄 Display your Gumroad products on your Next.js website (February 26, 2023)If you sell some products on Gumroad and also have a personal website, maybe you’d like to automatically list your products on this website. And if your website is based on Next.js, you can do it pretty easilly using Gumroad API.