This article is part of an entire Next.js series of articles that I am putting together to help you become a Next.js pro and start building blazing fast React apps.
š”Ā If you donāt want to miss out on any of the tutorials, signup for my newsletter by clicking here or head over to DailyDev.io for more.
On this issue, we will be learning about how Next.js enables high-performant websites by pre-rendering every page by default instead of having it all done by client-side JavaScript, like regular React apps usually do.
š” You can find the source code for this project here.
So letās get started!
Pre-requisites
- Node ā„ 12
- React Basics
Quick Recap
Up to this point, we have been talking about the concept of pages, how to represent them within our Next.js project, and how to make them either static or dynamic so that Next.js would know how to render and match specific URLs to their corresponding React components.
We then fired up our development server by running npm run dev
and waited for a browser window to pop up with our app running at http://localhost:3000
. Great! š
But one thing we have not done is dive deeper into how Next.js is assembling those pages and serving them back to us when we visit some URL. And better yet, how the production build of our app differs from the development environment we are running locally. And this is really where Next.js shines.
Pre-rendering
āWhat is pre-rendering?ā you might ask. Pre-rendering is the act of taking a page in the application and generating the plain HTML for it beforehand, instead of letting the client-side handle the bulk of the work. The HTML is then also shipped with minimal JavaScript code that will run in the client and that is necessary to make that page fully interactive.
This process helps solve two of the main downsides normally associated with React apps and general Single Page Applications (SPAs):
- shockingly low Search Engine Optimization (SEO) capabilities, since all pages and transitions are handled by the client through JavaScript code and, therefore, not crawlable by Search Engines
- heavy loads for the clients as they have to download and run the entire application on the browser which quickly presented problems as the applications became larger and more interactive
How Next.js Handles Pre-Rendering
Next.js will pre-render every page, by default. And it can happen in two different ways, the difference is when it generates the HTML for a page:
- Static Generation: The HTML is generated at build time and is reused on every request for that page.
- Server-side Rendering (for another article): The HTML for a page is generated on each request.
Both of these options will offer the benefits we discussed in the previous section but they can be used for different use cases upon different needs and you can even develop hybrid approaches within the same application by statically generating most pages and server-side rendering others.
The best and most performant choice for serving a web application is by statically generating all the applicationās pages since they can be easily cached in a Content Delivery Network (CDN) and boost performance by serving them closest to the requesting client. However, in some cases, Server-side Rendering might be the only option.
For now, letās take a look at how you can achieve Static Generation within or dog app.
Static Generation
Using Static Generation, the HTML for a page is generated at build time when we run the next build
command. That generated HTML is then served and reused whenever the page is requested.
There are two ways to statically generate pages, with or without data from external sources.
Static Generation without data
This is the most basic use case for a Next.js page, as it is the default behavior of the framework.
A simple component exported from a file in the pages
folder that does not need to fetch any external data before being pre-rendered generates a single HTML file during the build time.
An example would be the individual dog pages we created in our first tutorial on Next.js Basic Routing:
const Doggo: NextPage = () => {
return (
<div>
<main>
<h1>
This is a Doggo.
</h1>
<Image alt="This is a doggo" src='google.com' width={520} height={520}/>
<p style={{color: "#0070f3"}}><Link href="/">Back Home</Link></p>
</main>
</div>
)
}
export default Doggo;
Static Generation with Data
Then there is Static Generation dependent on fetching external data for pre-rendering. You can imagine two different use cases for needing to fetch external data for rendering pages:
- Your page content depends on external data.
- Your page paths (existing routes) depend on external data.
Scenario 1
We can think of an example within our doggo app where our page content will depend on external data. We made our page dynamic in the last tutorial, so all dogs are rendered by the same React Component. But all dogs have different information to be rendered on the page, therefore, the pages for each dog have distinct content.
Letās assume the following snippet of our updated dog page:
// Need to get a dog from the API
const Doggo: NextPage = ({ dog }) => {
return (
<div>
<h1>This is a {dog.name}.</h1>
<Image
alt="This is a doggo"
src={dog.imageURL}
width={520}
height={520}
/>
<p>{dog.description}</p>
</div>
);
};
export default Doggo;
To render each dog page with the correct data, we need to provide that specific dog data to our React Component.
To do this in Next.js, we will export an async
function with a specific name, getStaticProps
within the same page where the React Component representing the page is exported. This function will be called at build time when pre-rendering the page, and you can pass the necessary fetched data to the pageās props
.
const Doggo: NextPage = ({ dog }) => {
...
};
// This function gets called at build time
export const getStaticProps: GetStaticProps = async () => {
// Call an external API endpoint to get a dog
const res = await fetch("https://.../dogs/a-doggo");
const dog = await res.json();
// By returning { props: { dog } }, the Doggo component
// will receive `dog` as a prop at build time
return {
props: {
dog,
},
};
}
export default Doggo;
Scenario 2
Last time, we created a dynamic page within our app that enabled dynamic routes. With that, our app started responding to all requests for pages under /dogs/:id
. But instead of only exposing routes for existing dog ids, our application is matching every id, so it will never return a 404 - Not Found under that route.
In a real-world scenario, this does not make much sense. We would only want to render and serve pages for specific and individual resources that exist within our database.
So our page paths depend on external data and should be pre-rendered. Similar to before, Next.js allows you to declare a specific function within your page componentās file, whose sole purpose is to return a list of paths that this dynamic page should be rendered on, getStaticPaths
. This function also gets called at build time.
// This function gets called at build time
export const getStaticPaths: GetStaticPaths = async () => {
// Call an external API endpoint to get dogs
const res = await fetch("https://.../dogs");
const dogs = await res.json();
// Get the paths we want to pre-render based on dogs
const paths = dogs.map((dog: any) => ({
params: { id: dog.id },
}));
// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: false };
}
export default Doggo;
Now getStaticPaths
and getStaticProps
can work together to pre-render all pages for existing dogs, based on a single dynamic React Component.
Updating our Dog App
Now it is time to see this in action and power up our previously created dynamic page so that it can reach its full potential.
Creating a Dog Interface
Since we are using TypeScript to ensure type safety and easy development, we should make use of it and create an interface
to represent our dog and facilitate its usage through the app.
Letās create a new /definitions
folder to store our definitions files and create a dogs.d.ts
file with the following content, and now we have a simple representation of our dog object.
interface Dog {
id: number;
name: string;
description: string;
}
Creating our Dog Database
For simplicity, we will be creating a small in-memory structure to store our dogs and their information, so that Next.js can then access them and pre-rendered all the individual pages.
Letās create a /db
folder where we can store all our in-memory structures of data for ease of access. Inside we will create a dogs.ts
file and populate it with some structure data of some dogs using our previously created interface
.
export const dogs: Dog[] = [
{
id: 1,
name: 'Fido',
description: 'A friendly dog',
},
{
id: 2,
name: 'Rex',
description: 'A big dog',
},
{
id: 3,
name: 'Spot',
description: 'A small dog',
}
]
Updating our Dog Page Component
We will make some updates to our page component in order for it to become 100% dynamic, namely:
- Remove the use of the Next.js Router: Next.js will be giving us all the necessary information through the component
props
. - Create the
getStaticPaths
function to generate a list of string-based paths that represent only our available dogs. - Create the
getStaticProps
function to fetch the respective dog based on the information received in theparams
. - Update our page content to use the dog information present on the
dog
prop is it now receiving fromgetStaticProps
.
By the end, our React Component should look something like this:
import type { GetStaticPaths, GetStaticProps, NextPage } from "next";
import Link from "next/link";
import { dogs as dogsDB } from "../../db/dogs";
const Doggo: NextPage<{ dog: Dog }> = ({ dog }) => {
return (
<div>
<main>
<h1>This is {dog.name}.</h1>
<p>{dog.description}</p>
<p style={{ color: "#0070f3" }}>
<Link href="/dogs">Back to Dogs</Link>
</p>
</main>
</div>
);
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
if (!params || !params.id) {
return { props: {} };
}
const dog = dogsDB.find((dog) => dog.id === parseInt(params.id as string));
return {
props: {
dog,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
const dogs = dogsDB;
const paths = dogs.map((dog: Dog) => ({
params: { id: dog.id.toString() },
}));
return { paths, fallback: false };
};
export default Doggo;
Final Touch: Update Dogs Index Page
Just to end this on a high note, letās update our dogsā index.tsx
page so that it will list all existing dogs and link to their individual pages.
The same principles apply here, but since it is only a single non-dynamic page, we only use getStaticProps
and pass the dog list as props
to the page so that it can render the list.
import type { GetStaticProps, NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { dogs as dogsDB } from "../../db/dogs";
const Doggo: NextPage<{ dogs: Dog[] }> = ({ dogs }) => {
return (
<div>
<Head>
<title>Our Doggos</title>
</Head>
<main>
<h1>Check out our doggos.</h1>
<ul style={{ color: "#0070f3" }}>
{dogs.map((dog) => (
<li key={dog.id}>
<Link href={`/dogs/${dog.id}`}>{dog.name}</Link>
</li>
))}
</ul>
<p style={{ color: "#0070f3" }}>
<Link href="/">Back Home</Link>
</p>
</main>
</div>
);
};
export const getStaticProps: GetStaticProps = async () => {
const dogs = dogsDB;
return {
props: {
dogs,
},
};
};
export default Doggo;
Final Result
By the end, your app should look something like this. Pretty neat! š
Final Remarks
To experience the full power and speed of Static Generation, donāt forget to run the build command (npm run build
) followed by serving (npm run start
) the generated files. This is how the pages would be served in a production environment and cached by some CDN.
Running the project in npm run dev
mode will always build all pages on each request.
Notice how Next.js detected which pages were static and dependent on external data, generating exactly the routes defined by our in-memory database.
If you run into any trouble feel free to reach out to me on Twitter, my DMs are always open.
Next Steps: Keep an eye out for my following Next.js tutorials where we will go over much more in Next.js territory! If you donāt want to miss out on any of the tutorials, signup for my newsletter by clicking here.
Top comments (3)
Thanks for the great overview of the entire thing š
My pleasure š
Maybe I'm misunderstanding something but this does not appear to be true static-site rendering. Where are the output HTML files? This method still requires you to run NextJS on the server.
Static Site Rendering actually builds all the HTML, etc so that you can take it and drop it on some host or even a container like AWS S3 and it will just run without the need for a server-side component like NextJS serving the pages. That's the whole point of the word "static".