Navigation & Redirection

Next.js offers a few different ways to navigate between different routes.

First of all, there is a built-in <Link> component that extends the <a> element, which provides prefetching, client-side navigation, as well as other default optimizations.

text
1src/app
2├── error.jsx
3├── favicon.ico
4├── globals.css
5├── layout.jsx
6├── page.jsx           <=====
7└── users
8    └── [id]
9        └── page.jsx   <=====

app/page.jsx

jsx
1import Link from "next/link";
2
3export default async function Page() {
4  try {
5    const res = await fetch(`https://dummyjson.com/users`);
6    if (!res.ok) {
7      throw new Error("Users not found");
8    }
9
10    const data = await res.json();
11
12    return (
13      <ul>
14        {data.users.map((user) => (
15          <li key={user.id}>
16            <Link href={`/users/${user.id}`}>
17              {user.firstName} {user.lastName}
18            </Link>
19          </li>
20        ))}
21      </ul>
22    );
23  } catch (error) {
24    console.error("Server Component Error:", error.message);
25    return <p>Cannot find the users you requested.</p>;
26  }
27}

app/users/[id]/page.jsx

jsx
1import Link from "next/link";
2
3export default async function Page({ params }) {
4  try {
5    const id = (await params).id;
6    const res = await fetch(`https://dummyjson.com/users/${id}`);
7    if (!res.ok) {
8      throw new Error("User not found");
9    }
10
11    const user = await res.json();
12
13    return (
14      <>
15        <div>
16          Name: {user.firstName} {user.lastName}
17        </div>
18        <div>Email: {user.email}</div>
19        <div>Phone: {user.phone}</div>
20        <div>Birthday: {user.birthDate}</div>
21        <Link href={`/`}>Go back</Link>
22      </>
23    );
24  } catch (error) {
25    console.error("Server Component Error:", error.message);
26    return (
27      <div>
28        <p>Cannot find the user you requested.</p>
29        <Link href={`/`}>Go back</Link>
30      </div>
31    );
32  }
33}

The <Link> component works in both server and client components. The href attribute is required, and it should point to the target route, which works exactly the same as <a>.

Besides href, the <Link> component also accepts three optional attributes, replace, scroll, and prefetch.

  • replace decides if clicking on the link will add a new item to the browser history. Defaults to false.
  • scroll determines if the browser will maintain the scroll position when navigating to a new page. When set to false, the browser will not try to scroll to the first page element. Defaults to true.
  • prefetch can be set to true, false, or null. When set to true, the targeted route will always be prefetched when it enters the viewport. When set to false, prefetch will be disabled. When prefetch is set to null, which is the default behavior, prefetching will depend on if the route is static or dynamic.

Static routes are those that do not rely on external parameters (app/about/page.jsx), and dynamic routes rely on parameters to render the correct information (app/news/[slug]/page.jsx).

When prefetch is set to null, and the target route is a static route, then the whole route will be prefetched when it enters the viewport. If the targeted route is dynamic, then Next.js will prefetch the route segment up until the nearest loading.js.

loading.js is one of the optimization features of Next.js, which allows you to display a meaningful loading UI, while the page is being loaded. We will discuss it in detail later, and for now, you only need to know you can create a loading.jsx under every route segment like this:

text
1app
2└── dashboard
3    ├── page.jsx              <----- /dashboard
4    ├── loading.jsx           <----- Loading UI for /dashboard
5    └── reports
6        ├── page.jsx          <----- /dashboard/reports
7        ├── loading.jsx       <----- Loading UI for /dashboard/reports
8        └── [reportId]
9            ├── page.jsx      <----- /dashboard/reports/[reportId]
10            └── loading.jsx   <----- Loading UI for /dashboard/reports/[reportId]

This routing system is a bit more complicated than what we've seen so far, but it helps us understand what happens when you set prefetch to null for a dynamic route.

Imaging we have the following link:

jsx
1<Link href="/dashboard/reports/123">Report 123</Link>

We did not specify a prefetch, which means it will be kept as default null.

Next.js will prefetch static resources for /dashboard and /dashboard/reports, which is the closest route segment for /dashboard/reports/[reportId] that has a loading.jsx. The prefetching will stop there, and no resources will be fetched for /123.

If we remove the loading.jsx for route /dashboard/reports:

text
1app
2└── dashboard
3    ├── page.jsx              <----- /dashboard
4    ├── loading.jsx           <----- Loading UI for /dashboard
5    └── reports
6        ├── page.jsx          <----- /dashboard/reports
7        └── [reportId]
8            ├── page.jsx      <----- /dashboard/reports/[reportId]
9            └── loading.jsx   <----- Loading UI for /dashboard/reports/[reportId]

In this case, only /dashboard will be prefetched.

If no loading.jsx is created for any of the routes, then no route will be prefetched.

The useRouter hook in client component

useRouter is a built-in hook that enables you to programmatically navigate to different routes.

jsx
1"use client";
2
3import { useRouter } from "next/navigation";
4
5export default function Page() {
6  const router = useRouter();
7
8  return (
9    <button type="button" onClick={() => router.push("/users")}>
10      Show Users Page
11    </button>
12  );
13}

In this example, the router.push() method will instruct the browser to navigate to the route /users.

However, this seems like something we can easily do with the <Link> component, so what is the purpose of useRouter? This is because besides router.push(), useRouter offers a few more methods that allows you to create more complicated navigation logic.

For example, router.replace() triggers a navigation just like router.push(), but it will not add a new entry to the browser history.

jsx
1"use client";
2
3import { useRouter } from "next/navigation";
4
5export default function Page() {
6  const router = useRouter();
7
8  return (
9    <button type="button" onClick={() => router.replace("/users")}>
10      Show Users Page
11    </button>
12  );
13}

router.refresh() performs a refresh to the current route, the server side components will be rerendered, the data will be re-fetched, but the client side state will not be affected.

jsx
1"use client";
2
3import { useRouter } from "next/navigation";
4
5export default function Page() {
6  const router = useRouter();
7
8  return (
9    <>
10      . . .
11      <button type="button" onClick={() => router.refresh()}>
12        Refresh Page
13      </button>
14    </>
15  );
16}

router.back() and router.forward() navigates to the previous or the next route according to the browser history.

jsx
1"use client";
2
3import { useRouter } from "next/navigation";
4
5export default function Page() {
6  const router = useRouter();
7
8  return (
9    <>
10      . . .
11      <button type="button" onClick={() => router.back()}>
12        Go back
13      </button>
14    </>
15  );
16}

In most cases, the <Link> component is preferred over the useRouter hook, because it works in both server and client component, and provides a few optimizations that are not available with useRouter.

You should only opt to use useRouter when you need to create specific navigation features such as refreshing, going back or forward according to browser history, and so on.

Server side redirection

So far, we've been covering the situation where user clicks on a link or button, the apps captures that user input and navigates to another route.

But, there is a different scenario, and that is when the app dose something in the backend, and then decides to take the user to a different route without the user doing anything.

For example, a user visits a protected page, the backend examines the user's credentials, and if the user is unverified, they will ba taken to the login page instead of the page being requested. This is called a redirection, and it is initialized by the application, and not the user.

To perform redirection in Next.js, you can use the redirect() function in server components like this:

jsx
1import { redirect } from "next/navigation";
2
3export default async function Page() {
4  // Simulating user verification
5  const userVerified = await verifyUser();
6
7  if (!userVerified) {
8    redirect(`/login`);
9  }
10
11  return <div>. . .</div>;
12}

This redirect() function redirects the user to a new page by returning a 307 HTTP response code (temporary redirection), telling the browser to redirect to route /login.

Besides the redirect() function, there is a similar permanentRedirect(), which returns a 308 permanent redirect instead.

The difference between 307 temporary redirect and 308 permanent redirect is that permanent redirect tells the browser the requested page has been moved to the new route permanently. This information will be cached by the browser and search engine, allowing them to update their preference accordingly.

Setting up redirection in next.config.js

Lastly, instead of defining each redirection manually in the codebase, you can specify them in next.config.js, the configuration file for Next.js. This can be useful when you need to move multiple webpages to new locations. For example:

javascript
1/** @type {import('next').NextConfig} */
2const nextConfig = {
3  async redirects() {
4    return [
5      // Permanent redirect
6      {
7        source: "/about",
8        destination: "/",
9        permanent: true,
10      },
11      // Temporary redirect
12      {
13        source: "/hello",
14        destination: "/hi",
15        permanent: false,
16      },
17      // Path matching
18      {
19        source: "/community/:slug",
20        destination: "/news/:slug",
21        permanent: true,
22      },
23    ];
24  },
25};
26
27export default nextConfig;

You must provide three options for each redirection, source, destination, and permanent.

The source and destination accepts either hardcoded paths, or path matcher syntax such as :slug, :id, and so on.

permanent accepts a Boolean value, which can be set to true (308 permanent redirect) or false (307 temporary redirect).