In late 2022, the Next.js team launched their next stable version: Next.js 13. While this update came with a lot of improved functionality, one new feature is set to redefine the way you build with Next: the app directory beta.
The app beta is not used default. It needs to be enabled and can then be incrementally adopted. In this article, we're going to take a look at the new app structure and convert an existing Hygraph project to this new structure. We'll take this step by step and see how incremental adoption really works.
Required Knowledge
Why the app structure?
The new app directory structure isn’t just a reorganization of your project (though it is, and that reorganization feels powerful). This new methodology utilizes React Server Components to bring many new features and mental models. With Server components, a developer can do their business logic in a React component that doesn’t need to be sent to the client. This allows for a lot less JavaScript to be sent to the browser. If you have frontend needs, you can then use client components that get sent individually to the browser. This should reduce your bundle size down to a more manageable state. Just the code you need for interactivity.
The overall data fetching model has shifted, as well. Next has extended the native fetch() method from Node and the browser APIs and has added additional caching and deduplication functionality. This means you can create requests for data in various files, but the request will only fire to your API once. Fetching your blog posts to create a list on your homepage and to create the blog page list? Now that’s 1 API request, despite writing the code in multiple places. This gives a lot of developer convenience while creating better build and serve times.
There are even more new and improved features, but to use them, we need to be able to upgrade our project to use this new structure. It’s not as simple as moving your page files into the app directory, so let’s dig in and get things updated.
Setting up a Next.js 13 project to work with the app directory
To start, we need a Next.js project. While this process is possible with any Next project using data fetching, let’s start from our simple Next example from the hygraph-examples GitHub repository. Run the following commands:
npx degit hygraph/hygraph-examples/with-nextjs with-nextjs
cd with-nextjs
Before we install, we’ll need to update some dependencies in the package.json file. Let’s upgrade Next to ^13.0.0
and remove the react
and react-doc
dependencies.
Your package.json should now look like this:
{
"name": "hygraph-with-nextjs",
"private": true,
"license": "MIT",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"graphql-request": "^1.8.2",
"next": "^13.0.0"
}
}
Now we can start the development server by running npm run dev
. Immediately on loading the local version, we’re presented with the first update. Since this was previously not running Next 13, the Link component is structured wrong. In Next 13, we no longer need to have an anchor tag inside the Link component. This is a great update, but we need to change these references. The only location the Link component is used is the homepage in /src/pages/index.js
. Open that file, and remove the <a></a>
tags to get the site to run
// Old
<Link key={slug} href={`/products/${slug}`}>
<a>{name}</a>
</Link>
// New
<Link key={slug} href={`/products/${slug}`}>
{name}
</Link>
Now that the site runs, let’s update from the Pages directory to the new app directory.
Converting from /pages to /app
Before we can convert to the app directory, we need to let Next know that we’ll be using this experimental feature. To do that, we need a configuration file. In the root of our project, let’s create a next.config.js
file. Inside that file, add the following basic configuration.
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
};
module.exports = nextConfig;
From there, we can create our app directory. Inside the src directory, add an app
directory. We’ll be moving each of our routes to this directory. The nice thing is that each of our older routes will continue to work as we make these changes, since both directories can work at the same time.
Creating the root layout component
One of the first big changes to the overall structure is the need of a “root layout.” This will be the scaffolding of Markup for all pages. This can start very simply, but have any global information that each route will need. The RootLayout
function accepts a children object. That children object will contain the Server Components that we’ll add via each page we create.
export default function RootLayout({children}) {
return (
<html lang="en">
<body>
{ children }
</body>
</html>
)
}
With this layout, we can now add our first page.
Adding the homepage
Instead of the index.js
convention of the /pages
directory, each page in the app directory will be named page.js
. In the root of the app directory, create a new page.js
file. This will be our homepage.
Once you create this page, Next will thrown an error in the console. There is now a conflicting route: /src/pages/index.js
is the same route as /src/app/page.jsx
. For now, rename /src/pages/index.js
to oldIndex.js
. We’ll delete this file later, but this file is the blueprint for our new file.
Once we rename the file, we get a new error. The page.jsx
file is not exporting a React component. In fact, it’s exporting anything, since it’s blank. Let’s fix that and export a simple React component with an h1 of Products.
export default async function Page () {
return (
<>
<h1>Products</h1>
</>
)
}
Now the homepage should have the H1 instead of the product list from the old homepage. Let’s get the list of products back.
In the old index file, we export a function called getStaticProps
to get and pass the data to our page component. In the new structure, we don’t need to export anything or name things in any specific ways. Instead, we can create a regular async function to fetch our data.
Before our Server Component in the new page.jsx
file, create a new async function named getProducts
. This run a fetch request to the Hygraph endpoint for the project and return the products array. We can then run that function in our Server Component to loop through the data and display a link to each page.
import Link from 'next/link';
const getProducts = async () => {
const response = await fetch('https://api-eu-central-1.hygraph.com/v2/ck8sn5tnf01gc01z89dbc7s0o/master', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query: `{
products {
slug
name
id
}
}`
})
})
const {data} = await response.json()
return data.products
}
export default async function Page () {
const products = await getProducts()
return (
<>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>
<Link href={`/products/${product.slug}`}>{product.name}</Link>
</li>
))}
</ul>
</>
)
}
A few things have changed between our old version and new.
- Since we’re not using
getStaticProps
, we no longer need to structure our return in a specific way. Instead, we just return back the array of products - Using
fetch
instead ofGraphQLRequest
. By using fetch, we allow Next to cache the information and if we execute an identical fetch later, it won’t make a separate call, but instead use this data
Once you save this in, we have a functioning homepage. These links even take us to the pages associated with our old dynamic routes — since /app
and /pages
can work incrementally. That could be a fine stopping point, but let’s convert our dynamic route over, as well.
Converting the dynamic products route
Just like the homepage, we’ll start by setting up our structure. Instead of the dynamic route brackets happening on the file, hey happen on the directory. So the /pages/products[slug].js
will change to be /app/products/[slug]/page.jsx
. This allows for more flexibility and the ability to add custom, co-located features such as error pages, loading pages, and custom layouts.
Create the new file and add a simple component like we did for the homepage.
export default async function Page() {
return (
<>
<h1>This is a product</h1>
</>
)
}
This time, the application won’t immediately error, since the static pages aren’t being rebuilt, but if you restart Next, it will throw the same error as before about conflicting files. Delete or rename the file and we’ll move forward.
The overall structure of this file is significantly different. Just like the homepage, we no longer need the getStaticProps function, but we also don’t need the getStaticPaths either. These are all files that render on the server, so we can move all that information into a single async function that can happen at request time.
The new Server Component gives us a params object that we can use to get the slug for the current route. We can pass that into a new getProduct() function and use that as a variable in our query to Hygraph. This will fetch the data for that specific product. We can then use that in the Markup generated by our component to display the title, description, and price.
async function getProduct(params) {
const response = await fetch('https://api-eu-central-1.hygraph.com/v2/ck8sn5tnf01gc01z89dbc7s0o/master', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query: `{
product(where: {slug: "${params.slug}"}) {
name
description
price
}
}`,
variables: {
slug: params.slug
}
})
})
const {data} = await response.json()
return data.product
}
export default async function Page({params}) {
const product = await getProduct(params)
return (
<>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>{product.price}</p>
</>
)
}
We now have all the pages working together. Let’s take this one step further and add a custom 404 page for any products that aren’t found.
Setting up a custom 404 page
Because of the new app structure, we can colocate all of our relative files. In this case, if we want a custom 404 page, we can add a not-found.jsx
template in the /products/[slug]
directory.
The file will export a Server (or client) component, and for now, we’ll have a very simple message. In this case, since we’re looking for a product, let’s be specific and say “Product not found” instead of a simple “Page not found.”
export default function NotFound() {
return (
<>
<h1>404</h1>
<p>Product not found</p>
</>
)
}
Now, navigate to a route that doesn’t match any slug in Hygraph: try http://localhost:3000/products/sdlfkjl/
Instead of getting a 404 page, we receive an unhandled error. That’s because we need to tell Next when we know it’s not found. In this case, if our query to Hygraph returns no results, we need to throw a 404.
After calling the getProduct() function, we can check if product is defined and if not, run the notFound() function from next/navigation
. This will do a few things for us. It will redirect the browser to the NotFound component and automatically add a "noindex" meta tag to these routes. This is good SEO practice and will keep your site’s 404 pages from being indexed by search engines.
export default async function Page({params}) {
const product = await getProduct(params)
if (!product) notFound()
return (
<>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>{product.price}</p>
</>
)
}
Now, we have a fully-functioning 404 page!
From here, you can delete the /pages directory and move to working on your app. You’ll still use the pages directory for your API routes, as well as any static routes you want to use, but most things can be used to this new, more-powerful version of Next.
Next steps
With that, we have fully converted our simple Hygraph Next.js example from /pages to /app. At this point, I’d suggest taking look at all the different file types you can create in the colocated directories.
- Add a head.jsx file to specify meta data such as title, description, and more for each product page
- Add a loading.jsx file for loading page for longer queries.
- Add an error.jsx file to handle other error types.
Top comments (1)
I've personally been enjoying the App Router a lot (less syntax for me to remember, which I appreciate).
How has your experience been with it if you've used it?