Next.js 13 Crash Course - App Directory, Server Components & More

next.js Apr 05, 2023

In 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.