Next.js 13 Crash Course - App Directory, Server Components & More
Apr 05, 2023In this article, we are going to explore NextJS 13 and the new features including the app directory, server components, API route handlers and more. This post goes along with my NextJS 13 Crash Course YouTube video. You can find all of the code in the GitHub repo.
Now for those of you that are brand new to NextJS, I do have an existing crash course with the standard pages directory and the data fetching methods like getStaticProps
and getServerSideProps
. You can find that here: NextJS Crash Course. That's for people that are brand new to NextJS. We talk about what it is, etc. This crash course is for people that are familiar with NextJS and want to learn about the new features in NextJS 13.
Getting Setup
As many of you know, I like to start from scratch so that you can follow along. We're going to go ahead and create a new NextJS project using the following command:
npx create-next-app@latest next-13-crash-course
The questions that this CLI will ask may be different depending on when you're reading this. For instance, right now it will ask if we want to use the app
directory, where in the future, it may just use it and not ask, so be aware of that.
The first question here is if we want to use TypeScript. I'm going to say no, but feel free to use it if you want.
I'm also going to say no to the ESLint setup and no to using a src
folder. Again, feel free to use these if you want.
I am going to say yes to the app
folder option. This is the new feature in NextJS 13. We structure our project a bit differently with this option.
We will also use the default for the import alias.
Let's go ahead and cd into the project and open up VS Code (Or your editor of choice).
cd next-13-crash-course
code .
Let's also start up the dev server.
npm run dev
You should see a page like this:
Exploring The Folder Structure
Alright, now I understand that many of you already know NextJS and I'm mostly going to focus on the new stuff, but I will talk about some of the things that you already know, just for the people that are brand new to NextJS.
So package.json
is the same. We have React, React-DOM and Next. At this time, I am using NextJS 13.2.4. I'm sure that will change by the time you're reading this, but that's what I'm using.
In the next.config.js
you can see that the experimental
object has a property called appDir
. This is what we said yes to when we created the project.
I am also going to specify the distDir
here. This is where the build files will go. I like to put them in a folder called build
. This is the default, but I was having issues on my machine. It wasn't actually being created until I added this. So I will add the following:
const nextConfig = {
experimental: {
appDir: true,
},
distDir: 'build', // Add this
};
module.exports = nextConfig;
In the app
folder is where all of our code will go.
Clean Up
Let's wipe away the landing page output so that we can start fresh.
The homepage is the page.js
file that is directly in the app
folder. You can also use .jsx
for your pages and components, which is what I prefer. Open the page.js
file and replace the code with the following:
import Link from 'next/link';
const HomePage = () => {
return (
<>
<h1>Home</h1>
<Link href='/'>Home</Link> | <Link href='/about'>About</Link> | <Link href='/about/team'>Our Team</Link>
</>
);
};
export default HomePage;
This makes things much simpler for now and we can navigate to a couple pages that we will create.
The CSS
To make things even easier and have our project not look like crap, I created some simple CSS. Grab the CSS from the global.css
file in the GitHub repo and paste it into the global.css
file in your project.
Routes
Let's talk about routing and folder structure. In the past, we had a pages
folder and to create a route for let's say /about
, we would create a file in the pages
folder called about.js
. With the app
folder option, we no longer have a pages
folder. Instead, we create a folder with the name of the route that we want. For instance, let's create a folder named about
and inside of that folder, we will create a file called page.jsx
. This can also have a .js
extension. It doesn't matter. I just like to use .jsx
for React components.
Add the following code:
const AboutPage = () => {
return (
<>
<h1>About</h1>
<p>Some text about the page</p>
</>
);
};
export default AboutPage;
Now, go to localhost:3000/about
and you should see the text from the AboutPage
component. You can call your component function whatever you want. I use the convention of the name of the file and then Page
at the end. So about/page.jsx
would have a component function called AboutPage
.
Nested Routes
Folders are used to define routes instead of files. Files are used to create the UI for that route.
Let's say we want to create a nested route. Let's create a route for /about/team
. We can do that by creating a folder inside of the about
folder called team
. Then inside of the team
folder, we will create a file called page.jsx
. This will be the component that will be rendered for the /about/team
route.
layout.js
Another new feature in NextJS 13 is the layout.js
file. We have our main layout in the app
folder. This is where we would put our header, footer and anything that we want on every page.
Open the layout.js
and you can see that it's just a React component. It is server-rendered, in fact all components are server-rendered by default. I'll get to that soon though. This is where the html, head, and body tags are.
Metadata
When it comes to metadata, you should not manually add tags such as <title>
and <meta>
.
In older versions of NextJS, we used a head.js
file. This is no longer needed as we can now use the Metadata API.
We simply have this object where we can add meta data, Let's change the title and description. We can add other things like keywords, etc.
export const metadata = {
title: 'Welcome to Traversy Media',
description: 'The best place to learn web development',
keywords: 'web development, programming',
};
Now you can see that the title and description have changed. These will be true for all pages on the site.
We can also create layouts for any pages that we want by adding a new layout.js
file in the page folder. Let's add a layout to the about
page, just to see how this works. Create a new file called layout.js
in the about
folder. Add the following code:
export const metadata = {
title: 'About Traversy Media',
description: 'Check us out',
};
export default function AboutLayout({ children }) {
return (
<>
<div>THIS IS THE ABOUT LAYOUT</div>
<div>{children}</div>
</>
);
}
Now if you go to the about page, you will see the text and the new title and description. This is nice because if you have a page where you want a different container or a sidebar or whatever different layout, you can do that.
If all you need are different meta tags, creating a whole new layout is a little much. We can also use the metadata
object directly in the page.
Let's remove the about/layout.js
file and add a metadata
object to the about/page.jsx
file.
export const metadata = {
title: 'About Us',
};
const AboutPage = () => {
return (
<>
<h1>About</h1>
<p>Some text about the page</p>
</>
);
};
export default AboutPage;
Now you should see the new page title for about.
next/font
next/font includes built-in automatic self-hosting for any font file. This means you can optimally load web fonts with zero layout shift.
Let's add the Poppins
font to our project. In the layout.js
file, add the following:
import { Poppins } from 'next/font/google';
const poppins = Poppins({
weight: ['400', '700'],
subsets: ['latin'],
});
The weight can be a single string or an array of weights. Same with the subsets.
Then add the className
to whatever tag you want to use it on. I'm going to add it to the body
tag.
<body className={poppins.className}>
Now the entire site will use the Poppins
font.
Add Header
Let's start to add a little bit more to this website. Let's create a folder called components
and in that folder, create a file called Header.jsx
. Add the following code:
import Link from 'next/link';
const Header = () => {
return (
<header className='header'>
<div className='container'>
<div className='logo'>
<Link href='/'>Traversy Media</Link>
</div>
<div className='links'>
<Link href='/about'>About</Link>
<Link href='/about/team'>Our Team</Link>
</div>
</div>
</header>
);
};
export default Header;
Now open up the app/layout.js
file and add the Header
component to the top of the body.
import Header from '../components/Header';
export default function RootLayout({ children }) {
return (
<html lang='en'>
<body>
<Header />
<main className='container'>{children}</main>
</body>
</html>
);
}
Now the header will show on every page. We can now remove the links from the homepage and just keep the h1
or add some more text. It doesn't really matter.
Server vs Client Components
NextJS 13 uses a new feature called React Server Components
or RSC
. This allows us to render components on the server.
Advantages of RSC:
- Load faster - Don't have to wait for the JavaScript to load
- Smaller client bundle size
- SEO friendly
- Access to resources the client can't access
- Hide sensitive data from the client
- More secure against XSS attacks
- Improved developer experience
Just like with anything else, there are also disadvantages:
- Not as interactive
- No component state. We can not use the
useState
hook. - No component lifecycle methods. We can not use the
useEffect
hook.
Here is a chart from the NextJS website that shows when to use a server component vs a client component.
In NextJS 13, your components are server-rendered by default. This means, if you want to use the useState
hook for example and make it interactive, you need to make it a client component. We can do that simply by adding use client
to the top of the file.
You can test this by trying to import useState
into the Header.jsx
component. It will give you an error because it is a server component. If you add 'use client';
to the top, it will work because that makes it a client component.
Data Fetching
If you have used NextJS at all, then you are probably familiar with the three ways to fetch data in NextJS. We have getStaticProps
, getServerSideProps
and getInitialProps
.
With NextJS 13 and the App Router, we have a new and simplified approach. It is actually built on top of the fetch
api.
It's reccomended that you fetch data using server components. The reason for this is because you have more access to resources on the server, it keeps your app secure by preventing things like access tokens from being exposed to the client. It also reduces client-server waterfalls.
We are going to fetch data from the GitHub API. We are going to fetch a list of my repositories and display them on a page.
I want to display them nicely, so we are going to use the react-icons
package. Install it with npm i react-icons
.
Let's create a new page/component at app/code/repos/page.jsx
. Add the following code:
import Link from 'next/link';
import { FaStar, FaCodeBranch, FaEye } from 'react-icons/fa';
async function fetchRepos( ) {
const response = await fetch(
'https://api.github.com/users/bradtraversy/repos'
);
const repos = await response.json();
return repos;
}
const ReposPage = async () => {
const repos = await fetchRepos();
return (
<div className='repos-container'>
<h2>Repositories</h2>
<ul className='repo-list'>
{repos.map((repo) => (
<li key={repo.id}>
<Link href={`/code/repos/${repo.name}`}>
<h3>{repo.name}</h3>
<p>{repo.description}</p>
<div className='repo-details'>
<span>
<FaStar /> {repo.stargazers_count}
</span>
<span>
<FaCodeBranch /> {repo.forks_count}
</span>
<span>
<FaEye /> {repo.watchers_count}
</span>
</div>
</Link>
</li>
))}
</ul>
</div>
);
};
export default ReposPage;
Here, we are fetching the data from the GitHub API. We are using the async
and await
. We are then using the map
method to loop through the array of repos and display them. I also added a link to each repo that will take us to a page that will show more details about the repo. That way I can show you how dynamic routes work.
Now, in the Header
component, add a link to this page.
<div className='links'>
<Link href='/about'>About</Link>
<Link href='/about/team'>Our Team</Link>
<Link href='/code/repos'>Code</Link>
</div>
You should see a list of repos:
See how easy that was? No weird functions or hooks. We just fetch the data and it's available to us.
Loading Page
Sometimes it can take a while to fetch data. We can now add a loading page to show while the data is being fetched. This will show automatically. We don't have to add any conditions or anything.
Just create a new file named loading.js
or loading.jsx
in the app
folder. Add the following code:
const LoadingPage = () => {
return (
<div className='loader'>
<div className='spinner'></div>
</div>
);
};
export default LoadingPage;
There are styles in the global.css
to show a spinner. Now when the data is being fetched, you will see a spinner.
Let's slow the fetching of the data down so we can see the spinner. In the repos/page.jsx
file, add a setTimeout
function to the fetchRepos
function.
async function fetchRepos( ) {
const response = await fetch(
'https://api.github.com/users/bradtraversy/repos'
);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 seconds
const repos = await response.json();
return repos;
}
Now you should see the spinner for 1 second. We didn't have to add any code to show the spinner. It just works.
Dynamic Routes
Now let's look at dynamic routing. I want to have a page for each repo. We can do this with dynamic routes. This concept obviously is not new to Next 13, but it works a little differently.
First thing we'll do is create a new folder, not a file, but a folder called [name]
. This is a special folder name. It tells NextJS that this is a dynamic route. We can name it whatever we want, but it's best to use something that makes sense. We can pass in a repo name into the Github API and get the data for that repo.
Inside of the [name]
folder, create a new file called page.jsx
. Add the following code:
import Link from 'next/link';
const RepoPage = async ({ params: { name } }) => {
return (
<div className='card'>
<h2>{name}</h2>
<p>Repo details</p>
</div>
);
};
export default RepoPage;
To get the page name, we are using the params
object. We used name
but it could be anything else, such as id
. If you need query params, for instance if we had a query param of ?sort=desc
, then you would use searchParams
instead of params
.
Suspense Bounderies
To have more control over our components, we can use something called Suspense Boundaries. Let's say we are fetching multiple resources and one takes longer than the other. Right now, it would show our spinner from the loading.jsx
page until all the data is fetched. We can use Suspense Boundaries to show a different loader for each resource.
So what I'm going to do is have two components. One will fetch the standard data for the repo and one will get all of the directories in the repo with a link to each one. We're going to make it so the directories component will take 3 seconds longer than the standard data component.
Create a new component in the components
folder called Repo.jsx
. Add the following code:
import { FaStar, FaCodeBranch, FaEye } from 'react-icons/fa';
import Link from 'next/link';
async function fetchRepo(repoName) {
const response = await fetch(
`https://api.github.com/repos/bradtraversy/${repoName}`
);
const repo = await response.json();
return repo;
}
const Repo = async ({ name }) => {
const repo = await fetchRepo(name);
return (
<>
<h2>{repo.name}</h2>
<p>{repo.description}</p>
<div className='card-stats'>
<div className='card-stat'>
<FaStar />
<span>{repo.stargazers_count}</span>
</div>
<div className='card-stat'>
<FaCodeBranch />
<span>{repo.forks_count}</span>
</div>
<div className='card-stat'>
<FaEye />
<span>{repo.watchers_count}</span>
</div>
</div>
<Link href={repo.html_url} className='btn'>
View on GitHub
</Link>
</>
);
};
export default Repo;
We are fetching the repo of the name that is passed into the component. We are then displaying the data. We are also adding a link to the repo on GitHub.
Now create another component called RepoDirs.jsx
. Add the following code:
import Link from 'next/link';
async function fetchContents(repoName) {
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait for 3 seconds
const response = await fetch(
`https://api.github.com/repos/bradtraversy/${repoName}/contents`
);
const contents = await response.json();
return contents;
}
const RepoDirs = async ({ name }) => {
const contents = await fetchContents(name);
const dirs = contents.filter((item) => item.type === 'dir');
return (
<>
<h3>Directories:</h3>
<ul>
{dirs.map((dir) => (
<li key={dir.path}>
<Link href={`/${name}/${dir.path}`}>{dir.path}</Link>
</li>
))}
</ul>
</>
);
};
export default RepoDirs;
Here we are fetching the contents of the repo. We are then filtering out all the directories. We are then displaying a list of links to each directory. However, this will take 3 seconds to load.
Now bring both into the RepoPage
component:
import Repo from '@/app/components/Repo';
import RepoDirs from '@/app/components/RepoDirs';
Here we are using the @
alias to import the components. This is so we don't have to use relative paths.
Now, add the components and pass in the repo name from the params
object. We will also add a back button and a container:
const RepoPage = async ({ params: { name } }) => {
return (
<div className='card'>
<Link href='/code/repos' className='btn btn-back'>
{' '}
Back to Repositories
</Link>
<div className='card-content'>
<Repo name={name} />
<RepoDirs name={name} />
</div>
</div>
);
};
Ok, so what should happen is that you will see the spinner until all the data is fetched. So for 3 seconds, we won't see anything.
Let's add suspense boundaries around both components. First, import Suspense
from react
:
import { Suspense } from 'react';
Then add the Suspense
component around each component with a fallback
:
<Suspense fallback={<p>Loading repo...</p>}>
<Repo name={name} />
</Suspense>
<Suspense fallback={<p>Loading directories...</p>}>
<RepoDirs name={name} />
</Suspense>
Now, you should see the repo data pretty much right away, and you'll see the loader for the directories for 3 seconds and then those will display. This way, we can see the rest of the UI while we are waiting for the directories to load.
Caching and Revalidating
By default, fetch
will cache everything indefinitely. This is great for performance, but it can cause issues if you are fetching data that changes often. In the past, we had the getStaticProps
and getServerSideProps
functions to handle this. However, with NextJS 13 data fetching, we just need to set different options, depending on the type of data we are fetching.
We can use the revalidate
option to tell NextJS how often to check for new data.
Open the 'app/code/repos/page.jsx' file and add the revalidate
option like so:
const response = await fetch(
'https://api.github.com/users/bradtraversy/repos',
{ next: { revalidate: 60 } }
);
Also add it to the fetchRepo
function in the Repo
component:
const response = await fetch(
`https://api.github.com/repos/bradtraversy/${repoName}`,
{ next: { revalidate: 60 } }
);
and in the fetchContents
function in the RepoDirs
component:
const response = await fetch(
`https://api.github.com/repos/bradtraversy/${repoName}/contents`,
{ next: { revalidate: 60 } }
);
API Route Handlers
API routes have been replaced with route handlers
, which allow you to create custom request handlers for a given route using the Web Request and Response APIs. Route handlers are only available in the app
directory. They are the equivalent of API Routes inside the pages
directory meaning you don't need to use API Routes and Route Handlers together.
If you open the app/api
folder, you will see a hello.js
file. This is an example of a route handler. Let's take a look at the code:
export async function GET(request) {
return new Response('Hello, Next.js!');
}
Here we are exporting a function called GET
that takes in a request
object. We are then returning a new Response
object with the message we want to send back to the client. We can also use POST
, PUT
, PATCH
, DELETE
, and HEAD
methods.
If you use your browser or a tool like Postman, you can visit http://localhost:3000/api/hello
and you will see the message.
Courses API
What I would like to do is have a route that we can hit to get a list of my courses. The course data could be in any type of database or headless CMS. For this example, I will just use a JSON file to keep things simple.
Create a new folder called courses
in the api
folder. Then create a new file called data.json
and add the following data:
[
{
"id": 1,
"title": "React Front To Back",
"description": "Learn Modern React, Including Hooks, Context API, Full Stack MERN & Redux By Building Real Life Projects.",
"link": "https://www.traversymedia.com/Modern-React-Front-To-Back-Course",
"level": "Beginner"
},
{
"id": 2,
"title": "Node.js API Masterclass",
"description": "Build an extensive RESTful API using Node.js, Express, and MongoDB",
"link": "https://www.traversymedia.com/node-js-api-masterclass",
"level": "Intermediate"
},
{
"id": 3,
"title": "Modern JavaScript From The Beginning",
"description": "37 hour course that teaches you all of the fundamentals of modern JavaScript.",
"link": "https://www.traversymedia.com/modern-javascript-2-0",
"level": "All Levels"
},
{
"id": 4,
"title": "Next.js Dev To Deployment",
"description": "Learn Next.js by building a music event website and a web dev blog as a static website",
"link": "https://www.traversymedia.com/next-js-dev-to-deployment",
"level": "Intermediate"
},
{
"id": 5,
"title": "50 Projects in 50 Days",
"description": "Sharpen your skills by building 50 quick, unique & fun mini projects.",
"link": "https://www.traversymedia.com/50-Projects-In-50-Days",
"level": "Beginner"
}
]
Now create a courses/route.js
file and add the following code:
import { NextResponse } from 'next/server';
import courses from './data.json';
export async function GET(request) {
return NextResponse.json(courses);
}
We are bringing in the NextResponse
object from next/server
and the courses
data from the data.json
file. We are then returning a NextResponse
object with the json
method and passing in the courses
data.
Let's test it out. Start the dev server and visit http://localhost:3000/api/courses
. You should see the data.
In the frontend, I want these to show on the homepage. Instead of fetching right from the homepage, I will create a server component called Courses
that will fetch the data and pass it to the homepage.
Create a file at app/components/Courses.jsx
and add the following code:
import Link from 'next/link';
async function fetchCourses( ) {
const response = await fetch('http://localhost:3000/api/courses');
const courses = await response.json();
return courses;
}
const Courses = async () => {
const courses = await fetchCourses();
return (
<div className='courses'>
{courses.map((course) => (
<div key={course.id} className='card'>
<h2>{course.title}</h2>
<small>Level: {course.level}</small>
<p>{course.description}</p>
<Link href={course.link} target='_blank' className='btn'>
Go To Course
</Link>
</div>
))}
</div>
);
};
export default Courses;
Now bring it into the homepage, app/page.js
and embed it:
import Courses from './components/Courses';
const HomePage = () => {
return (
<>
<h1>Welcome to Traversy Media</h1>
<Courses />
</>
);
};
export default HomePage;
Getting Query Params
Now we I will show how we can send data via query params. Let's have a new route for searching courses at http://localhost:3000/api/courses/search?query=YOURQUERY
. Create a folder called search
in the api/courses
folder and create a new file called route.js
and add the following code:
import { NextResponse } from 'next/server';
import courses from '../data.json';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('query');
const filteredCourses = courses.filter((course) =>
course.title.toLowerCase().includes(query.toLowerCase())
);
return NextResponse.json(filteredCourses);
}
We can get the query params from the request
object. We are then filtering the courses based on the query. Let's test it out. Open postman or your browser and visit http://localhost:3000/api/courses/search?query=react
. You should see the filtered courses.
Getting Body Data
Now let's have a route that will allow us to add a new course. Of course, we are not using a database, so it will not actually stick. But it will show how we can get data from the body of a request.
I want to have an ID generated for the new course, so I will install the uuid
package. Run the following command:
npm i uuid
I want to be able to use the route http://localhost:3000/api/courses
and send a POST
request with the data in the body. Open the existing api/courses/route.js
file and add the following code:
import { v4 as uuidv4 } from 'uuid';
export async function POST(request) {
const { title, description, level, link } = await request.json();
const newCourse = { id: uuidv4(), title, description, level, link };
courses.push(newCourse);
return NextResponse.json(courses);
}
We get JSON body data by using the request.json()
method. We then create a new course object and push it to the courses
array. Let's test it out. Open postman and send a POST
request to http://localhost:3000/api/courses
with the following data in the body:
{
"title": "New Course",
"description": "New Course Description",
"level": "Beginner",
"link": "https://www.traversymedia.com"
}
You should see the new course added to the array. Of course, this is only in memory, but you could use a database if you wanted to.
Adding Search Functionality to the Frontend
We are going to have an issue if we try and create a search component and then use the Courses.jsx
server component. That's because when we implement search, we need to keep re-rendering the courses component. We can't do that because it's a server component. So what I'm going to do is change the component structure up a little bit. I think this will make things a bit more clear when it comes to when to use server components vs client components.
We are now going to do our fetching of courses from the homepage. So open up app/pages/index.js
and change it to the following:
'use client';
import { useState, useEffect } from 'react';
import Courses from './components/Courses';
import Loading from './loading';
const HomePage = () => {
const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchCourses = async () => {
const response = await fetch('http://localhost:3000/api/courses');
const data = await response.json();
setCourses(data);
setLoading(false);
};
fetchCourses();
}, []);
if (loading) {
return <Loading />;
}
return (
<>
<h1>Welcome to Traversy Media</h1>
<Courses courses={courses} />
</>
);
};
export default HomePage;
We are now fetching the courses with useEffect
in a client component. The loading.jsx
works automatically for server components. Since this is now a client component, I am using it manually. We used the useEffect hook to do the fetching. We are also using the useState hook to set the courses and loading state. We are then passing the courses to the Courses
component.
Change the courses component to simply display the courses being passed in:
import Link from 'next/link';
const Courses = ({ courses }) => {
return (
<div className='courses'>
{courses.map((course) => (
<div key={course.id} className='card'>
<h2>{course.title}</h2>
<small>Level: {course.level}</small>
<p>{course.description}</p>
<Link href={course.link} target='_blank' className='btn'>
Go To Course
</Link>
</div>
))}
</div>
);
};
export default Courses;
Search Component
Now we are ready to add our search component. Create a new file at app/components/CourseSearch.jsx
and add the following code:
'use client';
import { useState } from 'react';
const CourseSearch = ({ getSearchResults }) => {
const [query, setQuery] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const res = await fetch(`/api/courses/search?query=${query}`);
const courses = await res.json();
getSearchResults(courses);
};
return (
<form onSubmit={handleSubmit} className='search-form'>
<input
className='search-input'
type='text'
name='query'
placeholder='Search courses...'
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button className='search-button' type='submit'>
Search
</button>
</form>
);
};
export default CourseSearch;
We are now hitting the route for search and sending the query. We are then passing the results back to the homepage. We are then using the getSearchResults
function to pass the results back to the homepage.
Now we need to add the search component and implement it in the homepage component. Open up app/pages/index.js
and add the following code:
'use client';
import { useState, useEffect } from 'react';
import CourseSearch from './components/CourseSearch'; // Add this line
import Courses from './components/Courses';
import Loading from './loading';
const HomePage = () => {
const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchCourses = async () => {
const response = await fetch('http://localhost:3000/api/courses');
const data = await response.json();
setCourses(data);
setLoading(false);
};
fetchCourses();
}, []);
if (loading) {
return <Loading />;
}
// Add this function
const getSearchResults = (courses) => {
setCourses(courses);
};
return (
<>
<h1>Welcome to Traversy Media</h1>
<CourseSearch getSearchResults={getSearchResults} /> {// Add this line}
<Courses courses={courses} />
</>
);
};
export default HomePage;
Now you should be able to search for courses.
Conclusion
Hopefully, this helped you grasp some of the newer features of Next.js 13. I think server components are going to be a game-changer. I'm excited to see what people do with them. I think they will be a great way to build full-stack applications. I'm also excited to see what the future holds for Next.js.
Stay connected with news and updates!
Join our mailing list to receive the latest news and updates from our team.
Don't worry, your information will not be shared.
We hate SPAM. We will never sell your information, for any reason.