Course

Introduction to Next.js 13

Welcome to tutorial on NextJS 13. NextJS is a popular JS framework that's used to develop web applications.

NextJS vs React

Key things to note about React are
  • React is a JavaScript library for building user interfaces.
  • It provides a component-based architecture, allowing developers to create reusable UI components.
  • React focuses on the view layer of an application, handling the rendering and updating of components based on state and props.
  • React is agnostic to the server-side or client-side rendering approach and can be used in both scenarios.
  • It requires additional configurations and setup to handle routing, server-side rendering, and other functionalities that are not provided out of the box.
Key things to note about NextJS are
  • Next.js is a framework built on top of React that provides additional features and optimizations for building server-rendered React applications.
  • Next.js simplifies the development process by providing a file-based routing system and automatic code splitting for improved performance.
  • It provides a complete solution for building production-ready Full Stack applications , handling server-side rendering, serverless api routes and other common requirements without additional configuration.
  • built-in support for lazy loading and suspense
  • Next.js can be used for both small projects and large-scale applications, offering scalability and flexibility.

Features of Next.js 13

NextJS13 came with few important changes as mentioned below.

  • Server Components
  • Server Actions
  • New App directory and project structure
  • SEO and Meta tags
  • SSG and Server side render
  • Caching

NextJS 13 vs old versions

  • In old versions of nextJS, layouts, server components, streaming, turbopack were not available
  • getStaticProps and getServerSideProps were used in nextJS 12. But in Next 13, we do not use these methods. We can directly access data from the database or API. so we do not need these functions in nextJS 13
  • getStaticPaths is also replaced by getStaticParams in NextJS 13
  • In NextJS 13, app router is recommended to be used. Pages router also works but it is not recommended to use pages router in new apps.
  • Built in support for SEO metadata is added in nextJS 13.

Setting up environment

I will guide you through the step-by-step process of installing a Next.js project from scratch. Let's get started!

Prerequisites

  • nodejs
  • vscode
  • git

Installation

  • Launch vs code and Open folder where you want to create new project
  • Then execute below command npx create-next-app@latest
  • Prompts will be displayed asking you to enter project name and other details - say yes to all
  • Once finished, cd into project dir
  • then start app using command npm run dev
  • then app must be accessible on localhost:3000

How it works

  • In app directory, there is page.tsx file - that's home page.
  • Every app also has root layout which is used to wrap the app inside layout where we can keep header/footers etc

Congratulations! You have successfully installed and set up a new Next.js project from scratch. From here, you can continue building and expanding your application by adding more pages, components, and functionality.

File and Folder structure

Some important files and folders you need to know are mentioned below.
  • app directory
  • next.config.js
  • middleware.ts
  • .env
  • tsconfig.json
  • package.json
  • .gitignore

Creating first page

here is how you can create a page in the App Router in Next.js 13:
  • Create a new file called app/page.tsx.
  • Export a React component from the file.
  • The component will be rendered at the / route.
Here is an example of a page in the App Router:
1import React from "react";
2export default function Page() {
3  return <h1>Hello, Next JS 13!</h1>;
4}
Once you have created the page, you can access it at the / route in your browser.

Component Types

Before nextjs 13 we used only client components. But from nextjs 13 onwards, we can use client components as well server components.

Client vs Server

By default, all components in NextJS are server based components. In server based components, we can not use any client side objects like window, document, useEffect, useState etc. If you want to mark any component as client component, you need to use below directive at the top of file.
'use client'
Here is why and when we should use server components.
  • Server components have direct access to server which means you can directly access database or make api call.
  • Client side JavaScript bundle is smaller (and so page load is also faster) when we use server components. Because we do not need to send any JS that's required to fetch data from the server or database. We can still fetch data from client components but it is recommended to use server components to fetch data unless it is absolutely necessary to do it from the client components.
  • Server components are also faster and help in SEO - search engine optimization. The reason being, the component is already rendered on the server and the html markup is sent to client. So data is avaialble for crawlers immedietly.

CSR vs SSR

In NextJS, each client component is rendered twice. Once on the server (called as SSR) and then on the client (called as CSR). If the output of CSR and SSR is matching, then only hydration happens.

SSR Errors

SSR errors are those errors that may occur on server. For example - if you try to access window object before component is mounted, you will get error saying "object is not defined". Do not use client side objects (e.g. window, document, localStorage, sessionStorage, navigator, location) before the component is rendered in nextJS. If you do so you will get error saying object not found. e.g. window is not defined, document is not defined etc. Best place to access and use these objects is inside useEffect function. Because useEffect runs after the component has been mounted and DOM is painted in browser. In react component, you can access these objects before component is rendered because these objects are always available due to client side rendering of component. But in case of nextJS, client components are pre-rendered on server and since these objects are not available on server, we get error saying e.g. object location is not defined.

Hydration errors

During hydration process, the SSR and CSR output markup is compared. If the output from both CSR and SSR matches, then only hydration process is completed. Otherwise, you will see hydration errors in browser console.

Here are some tips that will help you prevent hydration errors. For more, you can refer  Hydration errors
  • Wrongly nested tags - e.g. li tag should not be child of p tag. It it is, then you may encounter this error. Another example is if you do not use tbody tag under table tag, you will get error. Also not wrapping li elements inside ul or ol element may cause this error.  In below example, we have used li tag inside p tag so we will get hydration error.
    1
    2<p>
    3  <li>wrongly nested tags</li>
    4</p>
    5        
  • Do not use client side objects when the component is rendering. All client side should go in the useEffect or event handler functions. Sometimes hydration errors only appear when you refresh the page. So make sure that refresh the page before ruling out all hydration errors in the page.
  • Sometimes locale settings of browser and server are different so You need to check that output is not changing based on settings like date and time.
  • iOS tries to convert email, phone in html page into links, which can cause this error. To avoid this, you can use below meta.
    <meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
  • Sometimes this error may come when you are using third party library. In such cases, you can use useEffect to load the component dynamically.
    1"use client";
    2import { useEffect, useState } from "react";
    3
    4export default function ApexChart() {
    5  const [Chart, setChart] = useState();
    6
    7  useEffect(() => {
    8    import("react-apexcharts").then((mod) => {
    9      setChart(() => mod.default);
    10    });
    11  }, []);
    12  //Chart will render only after component is rendered on Client side
    13  return Chart && <Chart {...props} />;
    14}

Rendering

In NextJS context, Rendering the component means executing the code of the component (You can think of it as generating html markeup for the component). This does not mean painting the DOM in browser window. Components can be rendered at build time or run time (also called as request time).

Static rendering

When the components are rendered at build time, it is called as static rendering.

Dynamic rendering

When the components are rendered at run time (request) time, it is called as dynamic rendering.

Server vs Static vs SSG

When you execute npm run build command, nextJS marks each page as static, SSG or server based.

  • Static - pre-rendered at build time and served as HTML files. No need of server here as everything is static. Pages can be cached and served by CDN.
  • SSG - static site generation - pre-rendered at build time and served as HTML files. These pages fetch data and generate content dynamically. ISR (Incremental site generation) occurs when the page generated using SSG is rebuilt with fresh data. So server is required for revalidating pages.
  • Server - Server-rendered pages are generated on-demand at runtime for each user request. Every request is sent to server. No caching at all!!

Routing

Routing is nothing but how you access pages in the nextJS application. To create pages, you need to create special file page.tsx . To creat a nested route, got to app directory and then you can create folders. e.g. to create route like /tech/mobile, you will need to create 2 folders tech and then mobile. Then you can create page.tsx file in mobile directory.

Dynamic routes

Next.js supports dynamic routes, allowing you to create pages that can have dynamic segments in the URL. You can define a dynamic route by creating a folder with square brackets [] in the app directory. For example, if you create a folder named [id] under products directory, it will match routes like /products/123, where 123 can be any dynamic value.

generateStaticParams

The generateStaticParams function is used to generate static files for the dynamic routes. This is similar to getStaticPaths() function in older versions of NextJS.

Route groups

You can group routes using (logical-folder) syntax. Main benefit of grouping the routes is that we can have nested layouts in the same route segment level. e.g. /app/abc/page.tsx and app/xyz/page.tsx will share root layout. But If you want to apply another layout without affecting URL path structure, we can use route group. /app/(mygroup)/abc/page.tsx and /app/(mygroup)/xyz/page.tsx can share the nested layout created at /app/(mygroup)/layout.tsx

Loading UI

Special file loading.tsx can be used to provide better user experience by showing loading status before actual data is loaded. React suspense is used behind the scene when using loading.tsx

Streaming

With Streaming, each component in app is considered as seperate chunk. Each component can be loaded independently. This is different to SSR. With SSR, all components that are a part of page need to be rendered on server and then all data is sent to client. Streaming is much faster than SSR because with streaming multiple components can load data in parallel.

Error Handling

Error.tsx file can be used to wrap the page inside react error boundry.
1'use client'
2 
3 export default function Error({
4   error,
5   reset,
6 }: {
7   error: Error
8   reset: () => void
9 }) {
10   return (
11     <div>
12       <h2>Error occured. You messed up something!</h2>
13       <button onClick={() => reset()}>Render the component again!</button>
14     </div>
15   )
16 }

Middleware

Middleware is used to perform some operations before request is processed and response is sent. It allows you to edit headers of request and response, redirect or rewrite urls.
1import { NextResponse } from 'next/server'
2import type { NextRequest } from 'next/server'
3
4export function middleware(request: NextRequest) {
5
6  //we can also conditionally match paths
7  /*
8  if (request.nextUrl.pathname.startsWith('/abc')) {
9    return NextResponse.rewrite(new URL('/xyz', request.url))
10  }*/
11
12  return NextResponse.redirect(new URL('/xyz', request.url))
13}
14
15
16//run function for below paths
17export const config = {
18  matcher: '/dashboard/:path*',
19}

Data Fetching

Fetching

In server components, we can fetch data right inside the component.
1async function getData() {
2  const res = await fetch('https://www.softpost.org')
3  return res.json()
4}
5 
6export default async function Page() {
7  const products = await getData()
8 
9  return <main>{products}</main>
10}

Caching

All fetch requests are cached in NextJS. You can also use react cache() function to cache the output of function call.
1import { cache } from 'react'
2 export const getBlog = cache(async (slug: string) => {
3   const blog = await db.blogs.findUnique({ slug })
4   return blog
5 })

Revalidating

Markup for Static routes is generated at build time. So all requests to get those routes are served from already built content. What if you want to regenerate the content for static routes? One option is you can rebuild site. But that's not a good approach. Better way to regenrate the static content is to revalidate the component. You can do this in 2 ways.
  • Background revalidation - In this approach, you can specify the duration in seconds, after which the page is re-created.
    1
    2  
    3  //for fetch calls, you can use below syntax
    4  fetch('https://www.softpost.org/blog/1', { next: { revalidate: 100 } })
    5  
    6  //if you are not using fetch, you can use revalidate const
    7  export const revalidate = 100 // revalidate this page every 100 seconds
    8
    9          
  • On demand revalidation
  • Drawback of background revalidation is that you will need to wait to wait for specified duration before fresh data is served to the client. With on demand revalidation, data can be regenerated instantly by calling revalidateTag.

Server Actions

Server actions are experimental feature. They are based on React actions. You just need to add "use server" directive at the top of function.
1"use client";
2import { saveToMongoDB } from "../../lib/mongo-blogs";
3
4export default function Page() {
5   async function addItem(data) {
6     "use server";
7
8      const cartId = cookies().get('cartId')?.value;
9      await saveToDb({ cartId, data });
10     console.log("data ", data);
11     await saveToMongoDB({ data });
12   }
13
14  return (
15    <form id="f1" action={addItem} onSubmit={handleSubmit}>
16      <div>
17        {/* <label htmlFor="title">Category:</label> */}
18        <input
19          type="text"
20          name="category"
21          id="category"
22          placeholder="Category"
23          autoComplete="off"
24          defaultValue=""
25          required
26        />
27      </div>
28      <button type="submit">Submit</button>
29    </form>
30  );
31}
32

CSS

You can apply css in NextJS app using global css file or css modules.

Global CSS

To import the global css file, you can use below syntax.
import "./globalapp.css";

CSS modules

CSS Modules allow you to write modular CSS styles for your components. It provides a way to scope CSS styles locally to a specific component, preventing style clashes and allowing for better encapsulation.

CSS files should have the .module.css extension. For example, styles.module.css. When you import a CSS module in your component, Next.js automatically generates unique class names for the styles defined in that module. These class names are scoped locally to the component, which means they won't conflict with styles in other components. To import the css modules, you can use below syntax.
import styles from "./tableofcontent.module.css"

Optimization

Images

You can use Image component provided by nextjs to load images.
1import Image from 'next/image'
2import myphoto from './xyz.png'
3 
4export default function Page() {
5  return (
6    <Image
7      src={myphoto}
8      alt="alternate info"
9      placeholder="blur"
10    />
11
12    <Image
13    src="https://cloudinary.com/abc.png"
14    alt="external image"
15    width={200}
16    height={200}
17  />
18
19  <Image
20        alt="data"
21        src={abc}
22        sizes="100vw"
23        style={{
24          width: '100%',
25          height: 'auto',
26        }}
27      />
28
29      <div
30        style={{ width: "100%", paddingBottom: "56.25%", position: "relative" }}
31      >
32        <Image alt="image" src="/index.png" fill={true}  style={{
33          objectFit: 'cover', 
34        }}/>
35      </div>
36
37  )
38}

Script

In Next.js, there are two ways to include external scripts in your application: using the script tag directly in your JSX code or using the Script component provided by Next.js.
  • script tag - This is the traditional way of including external scripts in HTML. - You can include this tag directly in your JSX code, just like you would in a regular HTML file.
  •  <script src="https://abcddd.com/ext-script.js"></script>
  • Script component - It loads external scripts in a more controlled and optimized way.
    1import Script from 'next/script';
    2
    3  function MyComponent() {
    4    return (
    5      <div>
    6        
    7        <Script src="https://abcddd.com/ext-script.js" strategy="lazyOnload" />
    8      </div>
    9    );
    10  }

Metadata

Metadata can be added in 2 ways.
  • Static metadata
    1import { Metadata } from 'next'
    2  let title = 'title';
    3  let description = 'description';
    4  let image = 'http://www.softpost.org/abc.png'
    5  export const metadata: Metadata = {
    6    title,
    7    description  ,
    8    category: 'Category',
    9    openGraph : {
    10      title,
    11      description,
    12      images: [
    13        {
    14          url: image,
    15        },
    16      ],
    17    },
    18    twitter : {
    19      title,
    20      description,
    21      site: "@reply2sagar",
    22      creator: "@reply2sagar",
    23      images: image,
    24    };
    25  }
    26   
    27  export default function Page() {}
    28  
  • using generateMetadata function
  • 1import { Metadata, ResolvingMetadata } from 'next'
    2 
    3  type Props = {
    4    params: { id: string }
    5    searchParams: { [key: string]: string | string[] | undefined }
    6  }
    7   
    8  export async function generateMetadata(
    9    { params, searchParams }: Props,
    10    parent?: ResolvingMetadata
    11  ): Promise<Metadata> {
    12    // read route params
    13    const id = params.id
    14   
    15    // fetch data
    16    const product = await fetch("https://.../id").then((res) => res.json())
    17   
    18    // optionally access and extend (rather than replace) parent metadata
    19    const previousImages = (await parent).openGraph?.images || []
    20   
    21    return {
    22      title: product.title,
    23      openGraph: {
    24        images: ['/some-specific-page-image.jpg', ...previousImages],
    25      },
    26    }
    27  }
    28   
    29  export default function Page({ params, searchParams }: Props) {}
    30  

Lazy Loading

To make a web app interactive, we need to send Java Script from Server to client (browser). In big applications, size of Java Script is huge so we need some kind of mechanism that allows us to load the JS only when it is required. That's when Lazy Loading comes into picture. Lazy loading can be implemented in 2 ways.
  • Dynamic Imports
  • Using React.lazy() with Suspense - next/dynamic is based on React.lazy() and Suspense
1'use client'
2 
3 import { useState } from 'react'
4 import dynamic from 'next/dynamic'
5  
6 // Client Components:
7 const ComponentX = dynamic(() => import('../components/X'))
8 const ComponentY = dynamic(() => import('../components/Y'), { ssr: false })
9  
10 export default function ClientComponentExample() {
11  const [state, setState] = useState(false)
12
13  return (
14    <div>
15      {/* Below component will be loaded when state becomes true */}
16      {state && <ComponentX />}
17      {/* SSR for below below component will be skipped */}
18       <ComponentY />
19    </div>
20   )
21 }
You can load external JS files lazily using import function.
1'use client'
2 
3 import { useState } from 'react'
4  
5 export default function Page() {
6   function clickHandler(){
7    const extenaljs = (await import('xyz.js'))
8   }
9   return (
10     <div>
11       
12     </div>
13   )
14 }

Backend

API Routes - Route Handlers

Route handlers can be created using the Web Request and Response APIs. Just create a file with name route.js or route.ts under app/api directory.
1import { NextResponse } from 'next/server'
2        import { cookies } from 'next/headers'
3
4 export async function GET(request: Request) {
5
6  let c =  cookies().get('id')
7
8   const res = await fetch('https://www.xyz.com', {
9     headers: {
10       'Content-Type': 'application/json',
11     },
12   })
13   const data = await res.json()
14   return NextResponse.json({ data })
15
16   /*
17   or you can use native Response object
18   return new Response('Hello, Next.js!', {
19    status: 200,
20    headers: { 'Set-Cookie': 'id=xyzx' },
21  })*/
22  
23  //redirect('https://softpost.org/')
24
25 }
Routes are dynamically evaluated if method is other than GET
1import { NextResponse } from 'next/server'
2 
3 export async function POST() {
4   const res = await fetch('https://www.xyz.com', {
5     method: 'POST',
6     headers: {
7       'Content-Type': 'application/json',
8     },
9     body: JSON.stringify({ id: "2894389" }),
10   })
11  
12   const data = await res.json()
13  
14   return NextResponse.json(data)
15 }
NextRequest and NextResponse are wrappers around native Request and Response objects.
1import { type NextRequest } from 'next/server'
2 
3 export async function GET(request: NextRequest) {
4   const requestHeaders = new Headers(request.headers)
5
6   /*
7   You can use native Response object
8   return new Response('Hello, Next.js!', {
9    status: 200,
10    headers: { 'Set-Cookie': 'id=xyzx' },
11  })*/
12
13 }

Database Handling

To interact with a MongoDB database in a Next.js application, we can use a MongoDB client library. Follow below steps.
  • Install mongodb and mongoose package. Mongoose package is optional. This is required only when you want to enforce the schema for the mongodb database.
  • Add function to connect to mongodb
    1import { MongoClient } from "mongodb"
    2
    3const uri = process.env.MONGODB_URI
    4const options = {
    5  useUnifiedTopology: true,
    6  useNewUrlParser: true,
    7}
    8
    9let client
    10let clientPromise
    11
    12if (!process.env.MONGODB_URI) {
    13  throw new Error("Please add your Mongo URI to .env.local")
    14}
    15
    16if (process.env.NODE_ENV === "development") {
    17
    18  if (!global._mongoClientPromise) {
    19    client = new MongoClient(uri, options)
    20    global._mongoClientPromise = client.connect()
    21  }
    22  clientPromise = global._mongoClientPromise
    23} else {
    24  client = new MongoClient(uri, options)
    25  clientPromise = client.connect()
    26}
    27
    28export default clientPromise
  • Use mongoclient promise to fire queries.
    1import clientPromise from '../../lib/mongodb'
    2
    3export async function getCourses() {
    4  try {
    5    let client = await clientPromise;
    6    const db =  client.db("awesomedb");
    7    const collection = await db.collection("courses");
    8    
    9    //const categories = await collection.distinct("category");
    10    //const blog = await collection.findOne({ slug });
    11    //const blogs = await collection.find({ category }).sort({ published: -1 }).toArray();
    12    //let result = await collection.insertOne(blog);
    13    //console.log("Object saved to MongoDB:", result.insertedId);
    14
    15    return  collection.find().toArray();
    16  } catch (err) {
    17    console.log("getCourses - error ", err);
    18    return err
    19  } finally {
    20
    21  }
    22}

Using Next Auth

We can use next-auth package for authentication. Please follow below steps
  • Install next-auth
  • Create Oauth client id and secret for provider e.g. Google or GitHub
  • Create api route /app/api/auth/[...nextauth]/route.ts
  • 1import NextAuth from 'next-auth'
    2import GoogleProvider from 'next-auth/providers/google'
    3
    4export const options={
    5    GoogleProvider({
    6          clientId: process.env.GOOGLE_CLIENT_ID,
    7          clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    8      }),
    9      session:{strategy: 'jwt' as SessionStrategy},
    10      secret : process.env.NEXTAUTH_SECRET
    11}
    12
    13const handler=NextAuth(options)
    14
    15export {handler as GET , handler as POST}
  • Getting session data on server
  • 1import {options} from "app/api/auth/[...nextauth]/route.js"
    2import {getServerSession} from "next-auth/next"
    3
    4async function page(){
    5  const session = await getServerSession(options)
    6  console.log(session.user)
    7}
  • Getting session data on client
  • 1'use client'
    2import { SessionProvider } from "next-auth/react";
    3export default SessionProvider
    4
    5
    6//Then you can import sessionProvider like below
    7import SessionProvider from "./components/NextAuthSession";
    8
    9export default function RootLayout({
    10  children,
    11  session,
    12}: {
    13  children: React.ReactNode;
    14}) {
    15  const myData = "Hello, Context!";
    16
    17  return (
    18    <html lang="en">
    19      <head>
    20      </head>
    21      <body>
    22      <SessionProvider session={session}>
    23      <main>
    24      <div> {children}</div>
    25    </main>
    26      </SessionProvider>
    27      <body>
    28
    29      </html>
    30      )
    31  }
    32
    33
    Then you can use it in client component.
    1
    2  import { signIn, signOut, useSession } from "next-auth/react";
    3  const header = () => {
    4    const { data: session, status } = useSession();
    5
    6  //status can be loading, authenticated or unauthenticated
    7  console.log("session ", session);
    8
    9  // {
    10  //   id: '648a47facf8d6a938cd0b5d7',
    11  //   name: 'Sagar Salunke',
    12  //   email: '[email protected]',
    13  //   image: 'https://lh3.googleusercontent.com/a/AAcHTtdVjC-wzENuiF3PLdoec8H7M9u33TxMXEpJdtam=s96-c',
    14  //   emailVerified: null
    15  // }
    16  
    17  
    18  return (
    19    <>
    20    {status === "authenticated" && (
    21      <button className="signButton" onClick={() => signOut()}>
    22        <FontAwesomeIcon icon={faUser} />
    23      </button>
    24    )}
    25
    26    {status !== "authenticated" && (
    27      <button
    28        className="signButton"
    29        onClick={() => signIn(undefined, { callbackUrl: "/" })}
    30      >
    31        <FontAwesomeIcon icon={faUser} />
    32      </button>
    33    )}
    34    </>
    35    )
    36  }

Deploying a Next.js app

To deploy a Next.js app, you have several options depending on your hosting preferences and requirements. Here are some common methods for deploying a Next.js app.

Deploying to Vercel

Vercel is the recommended hosting platform for Next.js apps. You can deploy your app to Vercel by linking your GitHub, GitLab, or Bitbucket repository to Vercel. Whenever you push changes to your repository, Vercel will automatically build and deploy your Next.js app. Visit the Vercel website (vercel.com) and follow their documentation to set up your deployment. Deployment to vercel platform was very easy for me. Ensure that all environment variables are set in vercel before deploying the app.

Deploying to other platforms

Next.js apps can be deployed to various hosting platforms, such as Heroku, Netlify, AWS, Google Cloud, DigitalOcean, or any server that supports Node.js applications. Set up a server or hosting environment, configure the necessary build and deployment steps, and deploy your Next.js app. The deployment process may vary depending on the hosting platform you choose, so refer to their respective documentation for detailed instructions.