• Labs icon Lab
  • Core Tech
Labs

Guided: Foundations of Next.js 14

Take your React skills to the next level with this 45-minute guided codelab on Next.js 14. Designed for developers with basic React and JavaScript knowledge, this session will introduce you to the powerful features of Next.js, including server-side rendering, static site generation, and dynamic routing. You'll build a simple, functional web app, exploring file-based routing, data fetching, and creating API routes. By the end, you'll have a solid foundation to build scalable, production-ready React applications with Next.js.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 2h 9m
Published
Clock icon Oct 01, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    1: Set up the Project

    Objective

    Get familiar with the Next.js project structure.

    Welcome to this guided codelab where you'll build a web application for GloboTicket, an online ticket sales platform, using Next.js 14. ### 1. Project Initialization (Offline Setup)

    This codelab already includes a basic Next.js project setup. If you want to set up a project from scratch in your local environment, you can use the command npx create-next-app@latest globoticket, but don't run it in this codelab environment, as the project is already set up for you.

    NOTE: Tests in the .tests folder are for verification of the tasks. Do not edit these. You should not need to edit the package.json file either. If you ever get stuck, follow the steps or check the solutions. The solution folder contains the files at the end state of the codelab, feel free to check them as well. Lastly, you might need to re-run the dev server with npm run dev if it starts throwing errors or does not pick up the changes after you edit your files. ### 2. Project Structure

    Next.js projects have a specific structure. Here's an overview of the key folders and files, do not worry if you don't understand everything yet, we will cover each part in detail in the following steps:

    • app/: This is the main directory for the Next.js project using the App Router introduced in Next.js 13 and above. It contains the application's pages and components. Next.js uses file-based routing, so each subfolder corresponds to a route in your app.
      • app/layout.tsx: The root layout component that wraps all pages. It's used for shared components like headers and footers.
      • app/page.tsx: The default page for the root route /. This is the Home page of your application.
      • app/global.css: The global CSS file applied to your application.
      • app/navbar.module.css: A CSS module file for the Navbar component that we'll create later.
      • app/util/: Contains utility functions and helpers used throughout the application. In this case it will provide us with mock data. This folder does not correspond to a route since it doesn't contain a page.tsx file.
        • app/util/events.ts: Contains a list of events that will be used in the application.
    • public/: Contains static assets like images, fonts, and other files that need to be served directly.
    • .tests/: Contains automated tests that verify the completion of tasks in each step. Do not edit these files.
    • package.json: Lists the project's dependencies and scripts. You should not need to edit this file.

    Note: The project uses TypeScript, so files have the .tsx extension for components and pages. Don't worry if you are not familiar with TypeScript, you can think of it as JavaScript with types, but it's use is not the focus of this codelab. ### 3. Run the Development Server

    Let's ensure that the project is set up correctly.

    In the terminal, start the development server:

    npm run dev
    

    This command should start the Next.js development server and make your application available at http://localhost:3000. Open the "Web browser" tab and navigate to http://localhost:3000. You should see the initial content of the GloboTicket application, for now this is just a Hello world example.

  2. Challenge

    2: Create the Home Page

    Objective

    Understand Next.js metadata, rendering as React components, and create the Home page for GloboTicket.

  3. Challenge

    3: Create the Events and Eventdetail Pages

    Objective

    Understand file-based routing in Next.js and create the Events and EventDetail pages for GloboTicket.

  4. Challenge

    4: Add Information to the Events and Eventdetail Pages

    Objective

    Understand how to use components and props in Next.js and add information to the Events and EventDetail pages for GloboTicket.

  5. Challenge

    5: Link Component

    Objective

    Understand how to use the Link component in Next.js and create links between pages.

  6. Challenge

    6: Image Component

    Objective

    Understand how to use the Image component in Next.js and optimize images for performance.

  7. Challenge

    7: Create the Layout Component

    Objective

    Understand how to create a Layout component in Next.js and use it to wrap pages.

  8. Challenge

    8: Style the Layout - CSS and Fonts

    Objective

    Understand how to style components in Next.js using CSS and fonts. ### 2. Add Fonts

    NOTE: this codelab is not connected to the internet. Next.js downloads fonts from Google fonts with each build, so you might get some errors in the console, and might not be able to see the UI changes, even though the fonts are cached.

    Next.js provides a way to customize your fonts by importing them from Google Fonts. This avoids the need to manually download and host the fonts yourself, or using a CDN to load them. Next.js will automatically optimize the font loading for performance, and serve them statically from your application server, reducing the number of network requests and improving the user experience.

    Let's add a custom font to the application by importing the Open Sans font from Google Fonts. Open the app/layout.tsx file and import the Open Sans font:

    import { Open_Sans } from "next/font/google";
    
    const openSans = Open_Sans({
      subsets: ["latin"],
    });
    

    This code imports the Open Sans font from Google Fonts and assigns it to the openSans variable. We can then use the openSans variable to apply the font to the HTML element in the layout. Update the html element in the app/layout.tsx file to use the Open Sans font, by using the className attribute:

    <html lang="en" className={openSans.className}>
    
  9. Challenge

    9: Server-side vs. Client-side Rendering

    Objective

    Understand the difference between server-side rendering and client-side rendering in Next.js. ### 2. State Management with Cookies and Server Actions

    In a more complex application, you would typically use a state management library like Redux, React Context or Zustand to manage the state of the application, and perhaps combine it with server-side session management. For this codelab we will use a simple cookie-based approach to manage the state of the shopping cart. It's worth noting that this is not a secure approach, as cookies can be manipulated by the client, but it's good enough for our purposes.

    Next.js provides a cookies object that allows you to read and write cookies in the browser. You can use this object to store the state of the shopping cart in a cookie, which will persist the number of tickets in the cart in the client's browser using a key-value pair.

    Next.js provides server actions, which are functions that run on the server side and can be called from the client side. This allows you to perform server-side actions like adding items to the shopping cart, and update the state of the application without reloading the page. Server actions are defined by creating a file that starts with the string 'use server' at the top, and exports async functions that can be called from the client side, for example on a button component.

    Let's create a server action to add tickets to the basket by creating the file app/events/[id]/actions.ts with the following code:

    "use server";
    import { cookies } from "next/headers";
    
    export async function addToBasket(numberOfTickets: number) {
      cookies().set({
        name: "ticket-count",
        value: numberOfTickets.toString(),
      });
    }
    

    This code defines a server action addToBasket that takes the number of tickets to add to the basket as a parameter. The function uses the cookies object from the next/headers module to set a cookie with the number of tickets in the basket. This cookie will be used to store the state of the shopping cart.

    Let's update the PlaceOrder component to call the addToBasket server action when the user clicks the "Place Order" button. Open the app/events/[id]/PlaceOrder.tsx file and update the PlaceOrder component to call the addToBasket server action:

    import { addToBasket } from "./actions";
    
    // ...
    
    const handleClick = () => {
      addToBasket(currentValue);
      alert(`Order placed for ${currentValue} tickets for event ${eventId}`);
    };
    

    This code imports the addToBasket server action from the actions.ts file and calls it when the user clicks the "Place Order" button. The addToBasket server action sets a cookie with the number of tickets in the basket, and the alert shows the number of tickets ordered.

    So now, let's update the Navbar component to show the number of tickets in the basket. Open the app/Navbar.tsx file and update the Navbar component to read the number of tickets from the cookie and display it in the header:

    // ...
    import { cookies } from "next/headers";
    
    export default function Navbar() {
      const cookieStore = cookies();
      const ticketCount = cookieStore.get("ticket-count")?.value || 0;
    
      return (
        // ...
        <p>
          <span>{ticketCount}</span> tickets
        </p>
        // ...
      );
    }
    

    This code imports the cookies object from the next/headers module and uses it to read the number of tickets from the cookie. The ticketCount variable is used to store the number of tickets in the basket, and is displayed in the header. The ticketCount variable is set to 0 if the cookie does not exist. Note that this is a server-side component, but cookies are stored on the client's browser. This works because the server sends a request to the client to update the cookies, and the client then includes the cookies in each request to the server, so the server can retrieve the cookies from the request headers and include them in the rendered component. Next.js handles all this automatically for us.

    NOTE: If you are having issues with the terminal disconnecting in the codelab environment, remove the line that creates the alert in the PlaceOrder file.

    If you now navigate to the /events/1 route and place an order for tickets, you should see an alert with the number of tickets ordered, and the number of tickets should be stored in a cookie in the browser. You should see that the basket in the navbar is updated with the number of tickets ordered.

    Of course, this is a very simple and incomplete example, as we're not storing the ticket IDs, and the customers can't remove tickets from the basket, but it should give you an idea of how you can use server actions and cookies to manage the state of the application in Next.js.

  10. Challenge

    10: Fetch Data and Handle API Calls

    Objective

    Understand how to fetch data and handle API calls in Next.js.

    2. Fetch Data in the EventDetails Page

    In a traditional Client Side application with React, you would typically fetch data from an API using fetch or a library like Axios, maybe using a useEffect hook to fetch the data when the component mounts. This works, but makes the page slower to load, as the data is fetched after the initial render, and leaves the client in charge of fetching the data. Usually, a server will be able to fetch the data faster, and can pre-render the page with the data, improving performance.

    In Next.js, you can fetch data in the server-side rendering process using the fetch function and by defining an async function in the page component. This function will be called on the server to fetch the data before rendering the page, and the data will be passed as props to the component. This allows you to pre-render the page with the data, improving performance and SEO. It's important to note that this will be done by the server at build time, so if the data changes after the page is built, the client will still get the same HTML with the old data. We will see how to solve this later, but now let's see this in action by fetching the event details in the EventDetails page.

    Open the app/events/[id]/page.tsx file and update the EventDetailsPage component to fetch the event details using the fetch function. We define a new async function findEvent that fetches the event details by ID from the API route, and then call this function in the EventDetailsPage component, instead of the findEventById function:

    // ...
    
    async function findEvent(eventId: string): Promise<Event | undefined> {
      const response = await fetch(
        `http://localhost:3000/api/events/${eventId}`,
      )
      if (!response.ok) {
        return undefined
      }
      const event = await response.json()
      event.date = new Date(event.date)
      return event
    }
    
    export default async function EventDetailsPage({ params }: Params) {
      const event: Event | undefined = await findEvent(params.id);
      // ...
    

    This code defines a new findEvent function that fetches the event details by ID from the API route using the fetch function. If the response is not ok, it returns undefined. Otherwise, it parses the JSON response and converts the date string to a Date object. The findEvent function is an async function that returns a Promise of the event details. We then call the findEvent function in the EventDetailsPage component to fetch the event details by ID.

    If you now navigate to the /events/1 route, you should see the event details fetched from the API route and displayed on the page. You can also try navigating to a non-existent event ID like /events/abc to see how the page handles the error.

    However, if you see the logs in the terminal, you will see that when you refresh the page, a call to GET /events/1 is logged for every request, but a call to GET/api/events/1 is only logged once. This is because the API route is only called once, when the page is built, and the data is fetched and pre-rendered. This is a good thing for performance, but it means that the data is not updated if it changes after the page is built. This is totally fine if your page displays static content, like a blog, where content will not change after the page is built, but it is not great if you need to display dynamic content. We will see how to solve this in the next step. ### 3. Dynamic Data Fetching

    To make sure that the data is always up-to-date, you can use dynamic data fetching in Next.js. This still fetches the data on the server side, but it will make sure that the page is revalidated and the data is refetched each time. To achieve this, we only need to make a small change to the fetch function in the findEvent function, by adding the cache: "no-store" option:

    const response = await fetch(`http://localhost:3000/api/events/${eventId}`, {
      cache: "no-store",
    });
    

    If you now refresh the page, you will see that the data is refetched each time, and the logs in the terminal will show a new call to GET /api/events/1 for each request. This is useful for pages that need to display real-time data, like a dashboard or a chat application, where you want to make sure that the data is always up-to-date. ### 4. Incremental Static Regeneration

    There's a third option for fetching data in Next.js, called Incremental Static Regeneration (ISR). This is a hybrid approach that combines the benefits of server-side static rendering and dynamic data fetching. With ISR, Next.js will pre-render the page with the data at build time, but will also revalidate the data and refetch it at a specified interval. This allows you to have the best of both worlds: fast page load times with pre-rendered data, and up-to-date data with revalidation.

    To use ISR, you can define a revalidate option in the fetch function that specifies the number of seconds to wait before revalidating the data. For example, to revalidate the data every 10 seconds, you can add the revalidate option to the fetch function:

    const response = await fetch(`http://localhost:3000/api/events/${eventId}`, {
      next: { revalidate: 10 },
    });
    

    If you now refresh the page, you will see that the data is refetched only if your request is more than 10 seconds later than the first one, and the logs in the terminal will show a new call to GET /api/events/1 every 10 seconds. This is useful for pages that need to display real-time data, but don't need to fetch the data on every request, like a news feed or a weather forecast.

  11. Challenge

    Conclusion

    Conclusion

    In this codelab, you learned how to build a simple ticketing application with Next.js. You learned how to create pages, components, and layouts, and how to use the Link and Image components. You also learned how to style components with CSS and fonts, and how to manage the state of the application with cookies and server actions. Finally, you learned how to fetch data and handle API calls in Next.js, and how to use server-side rendering, client-side rendering, and incremental static regeneration to improve performance and user experience.

    You now understand the foundations of building a web application with Next.js, and you can use this knowledge to build more complex applications with Next.js. You can now explore more courses and codelabs in Pluralsight to learn more about advanced topics in Next.js.

Julian is a Backend Engineer working with Java, Python, and distributed cloud systems. He has worked in Amazon, Google, and various startups, and focuses on first principles over tools.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.