Three New Next.js Features and How to Use Them
AWS Amplify recently added support for Next.js 10 features, including incremental static regeneration, optional catch all routes, and image optimization. In this post, we'll dig into what each of these features is, how to implement a fullstack app using them, and how to deploy them to AWS! Let's dive in.
Please note that I work as a Developer Advocate on the AWS Amplify team, if you have any feedback or questions about it, please reach out to me or ask on our Discord - discord.gg/amplify!
If you're new to Next.js, check out this tutorial first to get you started! I also wrote this tutorial on creating a fullstack Next.js app if you want to check that out.
Setup
First, let's create a Next.js app:
npx create-next-app next10-blog
Now, let's create our app backend. Head to the Amplify Sandbox and then "get started". Choose "data" on the next page, and start with the blog schema.
I deleted the "Blog" model and added the "content" field to the Post model.
Then, you can skip the "Test locally in your app" page and go straight to deploying with your AWS account. Follow the guided steps to deploy your app!
Once your backend has deployed, enter the Admin UI for your app and then click on "Local setup instructions" on the top right. Run the Amplify pull command into the Next app you created. Also, install the AWS Amplify libraries as well as TypeScript -- you don't need TypeScript for your code it's just for the generated DataStore models.
amplify pull --appId your-appID --envName staging
npm install aws-amplify typescript
I'm also going to generate some blog posts for my app. Click on "Manage app content" within the Amplify Admin UI. Under the "Actions" drop down you'll see an option to "Auto-generate data". Go ahead and generate 10 blog posts. You'll see titles and descriptions pop up!
Now it's code time! Open up the Next.js app that you generated a few steps ago. Open the _app.js file and ad the following. This will make it so that Amplify's frontend libraries are automatically tied to your backend resources that you created! We'll also enable server-side rendering.
import Amplify from 'aws-amplify'
import awsconfig from '../src/aws-exports'
Amplify.configure({ ...awsconfig, ssr: true })
Now, we'll implement the index.js
route -- this home page will list all of our blog posts and link them to a secondary page that will display one post. We'll use SSR for this route.
First, I'll import my data model from the generated src/models
directory. I'll also import the withSSRContext
function from Amplify -- this will allow us to run our query on the server side.
import { withSSRContext } from 'aws-amplify'
import { Post } from '../src/models'
Now, create a getServerSideProps function. Then we'll allow Amplify to run on the server with withSSRContext
, we'll provide it the request information as well. Then we'll run a query to get all of our blog posts! Finally, we'll return an object that provides our models as props! You can either convert to JSON manually or use the serializeModel
function from Amplify.
export async function getServerSideProps (context) {
const SSR = withSSRContext(context.req)
const models = await SSR.DataStore.query(Post)
return {
props: {
models: JSON.parse(JSON.stringify(models))
}
}
}
Now we can map through the posts and render them on the page!
export default function Home ({ posts }) {
return (
<div>
<Head>
<title>Amplify + Next</title>
<meta name='description' content='Amplify + Next!' />
</Head>
<main>
{posts.map(post => (
<div key={post.id}>
<a href={`/post/${post.id}`}>
<h2>{post.title}</h2>
</a>
</div>
))}
</main>
</div>
)
}
ISR
Now on to the new Next 10 features! First, we'll implement ISR, or incremental static regeneration. Normally when you use static site generation, the site builds once when you deploy your app. But in a lot of cases you want your static pages to update when your data changes. ISR enables that -- you provide a revalidation time to your getStaticProps
and then once that time window is reached the page will regenerate. Essentially, the pages are statically generated initially, and the initial users that hit the page before the provided regeneration time get served that statically generated site. Then, the next request to that page after the regeneration time is hit triggers the page to rebuild in the background -- the user that triggered the regeneration gets served the old version of the page but subsequent users get the new version. This is especially helpful in e-commerce scenarios, and in our case, a blog that you don't need to re-deploy every time you want to add a new post!
We'll create a page that displays one blog post. First, we'll create a post/[post].js
page component in the /pages/
directory. Let's start with the imports we'll need.
import { withSSRContext } from 'aws-amplify'
import { useRouter } from 'next/router'
import { Post } from '../../src/models'
Now, we'll create a getStaticPaths
function that will generate a static page for each post. We'll query all of our posts, map through them and then return them from the function. We'll also provide a fallback: true
here which will make it so that instead of immediately giving a 404 when a non-generated route is hit, Next.js will instead try and generate the page in the background and then render it.
export async function getStaticPaths() {
const SSR = withSSRContext()
const posts = await SSR.DataStore.query(Post)
const paths = posts.map(post => ({
params: { post: post.id }
}))
return {
paths, fallback: true
}
}
Now, we'll implement our getStaticProps
. We'll this time query for just one post using its id. Then we'll return the post in the props object, and we'll also add the revalidate
key. This will implement ISR for our page! I'll provide 10 which will make the revalidation time 10 seconds. You could change this value depending on your use case!
export async function getStaticProps(context) {
const SSR = withSSRContext(context.req)
const post = await SSR.DataStore.query(Post, context.params.post)
return {
props: {
post: JSON.parse(JSON.stringify(post))
},
revalidate: 10
}
}
Now, we'll render the post on the page! I'll use the router.isFallback
to render the loading indicator if a non-generated path is hit -- I only did this because I used fallback: true
!
export default function PostPage({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<h2>{post.title}</h2>
<p>{post.content}</p>
</div>
)
}
Then I'll push my code to GitHub. Then, I'll go back to the AWS Console page for my app. You should see the backend environments
page populated with your Admin UI link. Head over to the frontend environments
tab and you'll have the option to deploy your app!
Follow the guided deployment steps, you should be able to select your branch from GitHub and use the default build scripts detected from your package.json! You'll also see information about what was deployed -- in this case you'll have a Lambda@Edge function that will handle ISR for you!
Optional Catch All Routes
We have two more much quicker features to chat about, first optional catch all routes. These allow you to create a route that can have any parameters after it. We'll create one for an about page. /about
should render the page, but so should /about/hi
and /about/ali/spittel
. We can do this by creating a page component and then putting it in double brackets and adding three dots before it.
First, create the file for the page:
/pages/about/[[...about.js]]
Now, I'll implement the compnent. I'll use useRouter
from Next to get information about the route, then I'll render the route parameters on the page! Try /about
, /about/hi
and /about/ali/spittel
and see how this changes!
import { useRouter } from 'next/router'
import React from 'react'
export default function About(props) {
const routeData = useRouter()
return (
<div>
{JSON.stringify(routeData.query)}
</div>
)
}
Now push your code to GitHub and Amplify will automatically redeploy your frontend with the new about page!
Image Component
Finally, let's try out the Next.js Image
component. This component automatically enables image optimization with resizing, optimization, and serving different image types like webp when browsers support them. I added a picture of my dog Blair to the /public directory.
Then, I imported the Image
component in the index.js
file
import Image from 'next/image'
Then, I rendered her picture on the page!
<Image src='/blair.jpeg' alt='Fluffy white dog lying on a couch' height={200} width={150} />
I again pushed to GitHub to redeploy the site!
Conclusion
I hope this tutorial helped you implement a few of the new Next.js features and deploy them to AWS Amplify. If you want to take down your app, you can run amplify delete
from your CLI and your code will persist locally but it will no longer be deployed to the cloud. If you have any feedback on AWS Amplify or this tutorial, please let me know!