Let’s build an Events website with Next.js: Intro to Next.js
This is the first article of a series of posts where I will write about Next.js, the most complete framework for writing React.js applications. Through this guide, you will learn the basics of Next.js, as well as how to integrate it with GraphCMS, the GraphQL native headless CMS.
What is Next.js?
Next.js is a web framework for React.js, built and maintained by the Vercel team.
With Next.js, you can create both server-rendered and statically-generated websites, with out-of-the-box support for internationalization, image optimization, routing, TypeScript support, CSS and Sass support, code splitting, Serverless API Routes, and more. All of the above is offered out-of-the-box, with no further config needed.
Core concepts
File-system Routing
Unlike other React applications, where you may need to import a routing library, and write a routes config file associating each component with a route path, in Next.js it is a simple as adding files to the pages
directory and defaultly exporting a React component. There, the route path will be the filename. (i.e. pages/index.js
, pages/about.js
pages/blog/index.js
)
Matching dynamic parameterized routes is also possible by wrapping the filename with a bracket syntax (i.e. pages/blog/[slug].js
) and catch-all routes, which allows it to have as many subpaths as possible (i.e. pages/parametrized/[...paths].js
).
The following file structure:
.
|- pages/
|- |- about.js
|- |- blog/index.js
|- |- blog/[slug].js
|- |- index.js
|- |- parametrized/[...paths].js
Will match the following paths:
pages/about.js
=>https://my-website.com/about
pages/blog/index.js
=>https://my-website.com/blog
pages/blog/[slug].js
=>https://my-website.com/blog/hello-world
,https://my-website.com/blog/my-first-post
,https://my-website.com/blog/hello-world
pages/index.js
=>https://my-website.com
pages/parametrized/[...paths].js
=>https://my-website.com/parametrized/a
,https://my-website.com/parametrized/a/b
,https://my-website.com/parametrized/a/b/c
... and so on.
Data fetching
There is two ways to fetch data from a Next.js application. You can either generate static pages at build time, or fetch data from the server at run time.
1. Static pages generation
It is the recommended way to fetch public data coming from any data source. This can be a CMS, a database, or even a local or remote JSON file. One of the benefits of generating pages at build time is that you can put a CDN in front of it, and serve the already generated assets to the user.
To set up a page to be statically generated, export an asynchronous getStaticProps
function from it, returning an object with a nested props
object, which will be passed to the route component.
pages/blog/index.js
import cms from 'lib/cms'
export async function getStaticProps() {
const posts = await cms.getPosts()
/**
[
{
slug: 'hello-world',
title: 'Hello World',
body: 'This is the hello world blog post'
},
{
slug: 'my-first-post',
title: 'My first post',
body: 'This is my first post'
},
{
slug: 'my-second-post',
title: 'My second post',
body: 'This is my second post'
}
]
*/
return {
props: {
posts
}
}
}
export default function BlogHomePage(props) {
return (
<ul>
{props.posts.map(post => (
<li key={post.slug}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
)
}
When working with dynamic routes, an extra asynchronous function getStaticPaths
needs to be exported, which needs to return an object with a fallback
property (this will be explained later), and a nested paths
array. Each item of the array needs to be an object with a nested params
object, which will be received by getStaticProps
to know which object to fetch.
pages/blog/[slug].js
import cms from 'lib/cms'
export async function getStaticPaths() {
const posts = await cms.getPosts()
/**
[
{
slug: 'hello-world',
title: 'Hello World',
body: 'This is the hello world blog post'
},
{
slug: 'my-first-post',
title: 'My first post',
body: 'This is my first post'
},
{
slug: 'my-second-post',
title: 'My second post',
body: 'This is my second post'
}
]
*/
const paths = posts.map(post => ({
params: { slug: post.slug }
}))
/**
[
{ slug: 'hello-world' },
{ slug: 'my-first-post' },
{ slug: 'my-second-post' }
]
*/
return {
paths,
fallback: false
}
}
export async function getStaticProps(context) {
const post = await cms.getPostBySlug(context.params.slug)
/**
{
slug: 'hello-world',
title: 'Hello World',
body: 'This is the hello world blog post'
}
(the above can change depending on the requested slug)
*/
return {
props: {
post
}
}
}
export default function BlogPostPage(props) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
)
}
Note: the above will generate all blog posts at build time. If you don't want to generate all blog post static pages at build time, you can opt-in to Incremental Static Regeneration, which will be explained later on this article.
2. Server-side rendering
The other technique to hydrate a page with dynamic data is to render it from the server. This can be useful when working with private pages that can't be put on a CDN, and the data will change according to the request context. For that, an asynchronous function called getServerSideProps
will need to be exported from the route component, returning an object with a nested props
object.
pages/profile.js
import db from 'lib/db'
export async function getServerSideProps(context) {
const currentUserId = getCurrentUserIdFromCookies(context)
const user = await db.getUserById(currentUserId)
/*
{
name: 'John Doe',
somePrivateData: 'foo'
}
*/
return {
props: {
user
}
}
}
export default function ProfilePage(props) {
return (
<div>
<h1>{props.user.name}</h1>
<h2>Private data:</h2>
<p>{props.user.somePrivateData}</p>
</div>
)
}
Note: one of the caveats of server-side rendering is a page using it can't be cached by a CDN without additional configuration, and your page relies on a long-life server to be running. A workaround for this is to just export a component from the page route file, and do the fetching client-side using fetch
either on a useEffect
hook, or using a data fetching library such as SWR
.
API Routes
If you want to create serverless API endpoints for external consumers, or for client-side fetching, you can add API routes files to an api
directory within the pages
directory exporting a handler that receives a req
and a res
argument. It can also receive dynamic and catch-all parameters, just like a regular app route.
pages/api/profile.js
import db from 'lib/db'
export default async function profileApiRoute(req, res) {
const currentUserId = getCurrentUserIdFromCookies(req)
const user = await db.getUserById(currentUserId)
/*
{
name: 'John Doe',
somePrivateData: 'foo'
}
*/
res.json({
user
})
}
The above will create an API endpoint at https:my-website.com/api/profile that can be consumed from anywhere.
import { useState, useEffect } from 'react'
export default function ProfilePage() {
const [user, setUser] = useState(undefined)
useEffect(() => {
const fetchUser = async () => {
const res = await fetch('/api/profile')
const json = await res.json()
setUser(json.user)
}
}, [])
if (user === undefined) {
return <p>Fetching user...</p>
}
return (
<div>
<h1>{user.name}</h1>
<h2>Private data:</h2>
<p>{user.somePrivateData}</p>
</div>
)
}
Moving forward
The above core concepts are just a small taste of what Next.js can offer. In the upcoming articles in this series, you will continue learning how to use more techniques like image optimization, Incremental Static Regeneration, and Internationalization. Stay tuned!