App Router

Introduction

  • App router is mixture of multiple page and single page application

  • Each path has their own html page but when using next/link, the javascript will be loaded instead of html file

  • The initial page will be rendered on server, but for user interaction (e.g: useState, useEffect) , the page is needed to be hydrated with javascript file

Server & Client Side Component

  • When html page of Nextjs is loaded, the static content and data of of server component will be pre-rendered and fetched

  • The data of client component will then be hydrated through built javascript file

Example

layout.tsx
"use client";
// import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

// export const metadata: Metadata = {
//   title: "Create Next App",
//   description: "Generated by create next app",
// };
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        client layout
        {children}
      </body>
    </html>
  );
}
import ClientComponent from "./components/ClientComponent";
import ServerComponent from "./components/ServerComponent";

export default function Home() {
  return (
    <>
      <ClientComponent/>
      <ServerComponent/>
    </>
  );
}
ClientComponent.tsx
"use client";

import React from "react";

// Next.js fetch API in action
async function loadPosts() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");

 // return res.json();
 return ["this is client component"];
}

const ClientComponent =  () => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const [posts, setPosts] = React.useState<any[]>([]);
    React.useEffect(() => {
      loadPosts().then((posts) => setPosts(posts));
    }, []);
    return (

    <div className="post-list">
     Client Component
      {posts.map((post) => (
        <div key={post} className="post-listing">
          <p className="post-body">{post}</p>
        </div>
      ))}
    </div>
  );
};

export default ClientComponent;
ServerCompoenent.tsx
// Next.js fetch API in action
async function loadPosts() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");
   /* 
   * Since this is a server component, the below message  
   * won't be displayed in the browser's dev console.
   */ 
  console.log("Server Component fetching");
  // return res.json();
  return ["this is server component"];
}

// Next.js will invalidate the cache when a
// request comes in, at most once every 60 seconds.
export const revalidate = 60

const ServerComponent = async () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const posts:any[] = await loadPosts();
  return (
    <div className="post-list">
      Server Component
      {posts.map((post) => (
        <div key={post} className="post-listing">
          <p className="post-body">{post}</p>
        </div>
      ))}
    </div>
  );
};

export default ServerComponent;

Result

The pre-rendered html page
The hydrated html page
import Link from "next/link"
import ClientComponent from "../components/ClientComponent"

const OtherPage = () => {
  return (
    <div>
        <ClientComponent/>
        <Link href={"/"}>Back Home</Link>
    </div>
  )
}

export default OtherPage
Result of fetching RSC payload
  • If using Link instead of a tag for navigation. On UI level, it will be shown as a tag, but it will execute client side navigation behind the scene,

  • If the page contains server component, only RSC payload will be fetched, which is the result of server-side rendering and the position of client-side component with corresponding javascript file

Backend API

  • Nextjs can be used to act as full stack development, here is the example of implementing backend part. The file must be route.ts/js

api/route.ts
import { NextResponse } from 'next/server';

export function GET() {
  const randomNumber = Math.floor(Math.random() * 1000);
  const message = { message: `${randomNumber}` };
  return NextResponse.json(message);   
}
// Next.js fetch API in action
async function loadPosts() {
      const response = await fetch('http://localhost:3000/api');
      const json = await response.json();
      return await json.message;
    }

const ServerComponent = async () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const posts:any = await loadPosts();
  return (
    <div className="post-list">
     Server Component
        <div className="post-listing">
          <p className="post-body">{posts}</p>
        </div>
      
    </div>
  );
};
export default ServerComponent;

Caching

  • When using fetch api on Nextjs, there are several caches existing - request memoization, data cache

Request memoization

  • If you need to use the same data across a route, e.g using the same component, you do not have to fetch data in every time. Instead, it will fetch data once.

  • The cache lasts the lifetime of a server request until the React component tree has finished rendering.

import Link from "next/link";
import ServerComponent from "./components/ServerComponent";
export default function Home() {
  
  return (
    <>
      <ServerComponent/>
      <ServerComponent/>
      <Link href="/other">Go to other</Link>
    </>
  );
}
// Next.js fetch API in action
async function loadPosts() {
      const response = await fetch('http://localhost:3000/api',
        {cache:"default", next:{ revalidate: 0 }}
      );
      const json = await response.json();
      return await json.message;
    }

const ServerComponent = async () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const posts:any = await loadPosts();
  return (
    <div className="post-list">
     Server Component
        <div className="post-listing">
          <p className="post-body">{posts}</p>
        </div>
      
    </div>
  );
};
export default ServerComponent;
Result
  • To disable request memoization, we need integrate with AbortController

// Next.js fetch API in action
async function loadPosts() {
      const { signal } = new AbortController();
      const response = await fetch('http://localhost:3000/api',
        {signal, cache:"default", next:{ revalidate: 0 }}
      );
      const json = await response.json();
      return await json.message;
    }

const ServerComponent = async () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const posts:any = await loadPosts();
  return (
    <div className="post-list">
     Server Component
        <div className="post-listing">
          <p className="post-body">{posts}</p>
        </div>
      
    </div>
  );
};
export default ServerComponent;
After disabling

Data Cache

  • If fetch data with caching, the page will be built as static content (Static Generation), otherwise, the page will be built as dynamic

Cache options

  • Configure how the request should interact with Next.js Data Cache.

fetch(`https://...`, { cache: 'default' | 'force-cache' | 'no-store' })
  • default: Nextjs fetching will be treated same as force-cache, unless revaildate is specified

  • no-store: Next.js fetches the resource from the remote server on every request without looking in the cache, and it will not update the cache with the downloaded resource.

  • force-cache: Next.js looks for a matching request in its Data Cache.

    • If there is a match and it is fresh, it will be returned from the cache.

    • If there is no match or a stale match, Next.js will fetch the resource from the remote server and update the cache with the downloaded resource.

Incremental Static Regeneration (ISR)

  • The data of cache will be stale after a period of time, and the data will be obtained from data source rather than cache

// Next.js fetch API in action
async function loadPosts() {
    const { signal } = new AbortController();
    const response = await fetch('http://localhost:3000/api',
      {signal,  next:{ revalidate: 5 }}
    );
      const json = await response.json();
      return await json.message;
    }

const ServerComponent = async () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const posts:any = await loadPosts();
  return (
    <div className="post-list">
     Server Component
        <div className="post-listing">
          <p className="post-body">{posts}</p>
        </div>
      
    </div>
  );
};
export default ServerComponent;

Layout

  • The component in the layout file can be shared in the same directory and its nested page file

  • SideMenu is common use case to put it into layout file

app/layout.tsx
 "use client"
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import theme from "./themes/defaultTheme";
import {ThemeProvider} from "@mui/material"

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

// export const metadata: Metadata = {
//   title: "Create Next App",
//   description: "Generated by create next app",
// };

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider theme={theme}>
          main1
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Not Found & Error Boundary Page

  • Error page must be client component

app/not-found.tsx
import Link from 'next/link'
 
export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
      <Link href="/">Return Home</Link>
    </div>
  )
}
'use client' // Error boundaries must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

Route Folder & File naming

  • (Folder name) can be used without affecting the path

  • page.tsx refer to the page

  • [slug] refer to dynamic page

[slug]/page.tsx
export default async function Page({
    params,
  }: {
    params: Promise<{ slug: string }>
  }) { 
    const slug = (await params).slug;

    return (
      <div>My Post: {slug}</div>
    )
  }

Loading

  • The data fetching support Suspense of react

page.tsx
import { Suspense } from "react";
import axios from "axios";
export default async function Page({
    params,
  }: {
    params: Promise<{ slug: string }>
  }) {

    async function getMovies() {
      const {data} = await axios.get(
        `https://fakestoreapi.com/products`
      );
      // await new Promise((resolve) => setTimeout(resolve, 2000));
      return data;
    }

    const results = await getMovies();
    console.log(results);
  
    const slug = (await params).slug;

    return (
    <Suspense>
      <div>My Post: {slug}</div>
    </Suspense>
    )
  }
loading.tsx
import React from 'react'

const Loading = () => {
  return (
    <div>Loading</div>
  )
}

export default Loading

Parallel Route

  • Parallel Routes allows you to simultaneously or conditionally render one or more pages within the same layout.

  • Each route can actually be treated as component on layout

  • Each route nested route pattern must be the same

@user/page.tsx
import React from 'react'

const UserPage = () => {
  return (
    <div>User Page</div>
  )
}

export default UserPage
page.tsx
import React from 'react'
import TestCompoent from './components/test'
import Link from 'next/link'

const Main = () => {
  return (
    <div>
      Main Page
    </div>
  )
}

export default Main
layout.tsx
"use client"

import React from "react"

export default function Layout({
  user,
  children, // will be a page or nested layout
  }: {
    children: React.ReactNode
    user: React.ReactNode
  }) {
    return (
      <>
        /* User */
        {user}
        /* Main */
        {children}
      </>
    )
  }

Last updated

Was this helpful?