iT邦幫忙

2021 iThome 鐵人賽

DAY 5
1
Modern Web

從零開始學習 Next.js系列 第 5

Day05 - 使用 Link 實作換頁

Link

在了解了 Next.js 的三種路由方式後,接下來就讓我們來聊聊怎麼在 component 切換頁面吧!

在 Next.js 中切換頁面是用 client-side routing 的方式,其體驗會很像是 SPA (Single Page Application),而不是像傳統的 SSR 在切換頁面時畫面都會有重新載入的感覺。

Next.js 提供了 <Link /> 這個 component,讓我們可以在 Next.js 做到像是 SPA 的體驗。還記得 Next.js 有三種路由的方式,分別為 static routes、dynamic routes、catch all routes,對於這三種不同的路由方式,在定義 <Link /> 都大同小異。

import Link from "next/link";

function Home() {
  return (
    <ul>
      <li>
        <Link href="/post">
          <a>切換至 pages/post/index.tsx</a>
        </Link>
      </li>
      <li>
        <Link href="/post/123">
          <a>切換至 pages/post/[postId].tsx</a>
        </Link>
      </li>
      <li>
        <Link href="/post/2021/12/31">
          <a>切換至 pages/post/[...date].tsx</a>
        </Link>
      </li>
    </ul>
  );
}

export default Home;

以上方的範例來說, <Link /> 傳入的 href 都是對應一個 page 的 url:

  • /post 對應的是 pages/post.tsxpages/post/index.tsx
  • /post/123 對應的是 pages/post/[postId].tsx
  • /post/2021/12/31 對應的是 pages/post/[...date].tsx

物件形式的 href

href 不僅僅可以傳入 url 字串也能夠接受物件形式的 url 物件,我們透過 TypeScript 的型別定義來看看 href 可以傳入的數值有哪些:

type Url = string | UrlObject;

interface UrlObject {
  auth?: string | null | undefined;
  hash?: string | null | undefined;
  host?: string | null | undefined;
  hostname?: string | null | undefined;
  href?: string | null | undefined;
  pathname?: string | null | undefined;
  protocol?: string | null | undefined;
  search?: string | null | undefined;
  slashes?: boolean | null | undefined;
  port?: string | number | null | undefined;
  query?: string | null | ParsedUrlQueryInput | undefined;
}

從 Next.js 的型別定義中,我們可以看到 UrlObject 接受的參數非常多種,能夠用非常彈性的方式定義路由的 url。

我們用上面比較簡單來的例子改寫成 UrlObject

<li>
  <Link
    href={{
      pathname: "/post/[postId]",
      query: { postId: "123" },
    }}
  >
    <a>切換至 pages/post/[postId].tsx</a>
  </Link>
</li>
<li>
  <Link
    href={{
      pathname: "/post/[...date]",
      query: { date: ["2021", "12", "31"] },
    }}
  >
    <a>切換至 pages/post/[...date].tsx</a>
  </Link>
</li>

可藉由 pathname 以相似 page 的方式傳入 url,並在 query 傳入可以從 useRouter() 中拿到的值:

  • /post/[postId] 對應的是 pages/post/[postId].tsx
  • /post/[...date] 對應的是 pages/post/[...date].tsx

動態生成 href

既然 href 都能夠接受 stringUrlObject 兩種定義路由的方式,當然也能夠接受動態生成 url 的方式。

例如貼文列表裡面包含了許多貼文的連結,因此可以用 map 來生成許多的 <Link />

import Link from "next/link";

const Posts = () => {
  const posts = [{ id: "1" }, { id: "2" }, { id: "3" }];
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/post/${post.id}`}>
            <a>Post id: {post.id}</a>
          </Link>
        </li>
      ))}
    </ul>
  );
};

export default Posts;

但有一點必須要注意的是, <Link /> 這個 component 只能接受裡面有一個 child node,而且 child node 只能是 string 或是 <a> ,如上方的範例,假設把 <a> 拿掉以後 Next.js 便會報錯。

因為對於 react 來說, Post id: {post.id} 是會分成兩個 node,所以 Next.js 便無法順利的編譯。

Imperatively routing

在許多情況下 <Link /> 已經夠用,但是也許有些使用情境,我們需要延遲切換頁面的事件,例如在點擊按鈕時想要觸發 google analytics 的事件後,再觸發換頁。

所以,我們就可以用 useRouter 中的 router.push 來幫助我們達成這件事:

import { useRouter } from "next/router";

import ga from "../lib/ga";

const Posts = () => {
  const posts = [{ id: "1" }, { id: "2" }, { id: "3" }];
  const router = useRouter();

  const handleRouteChange = (post_url: string) => {
    ga.event({ action: "click_post_link", params: { post_url } });
    router.push(post_url);
  };

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <button onClick={() => handleRouteChange(`/post/${post.id}`)}>
            <a>Post id: {post.id}</a>
          </button>
        </li>
      ))}
    </ul>
  );
};

export default Posts;

Shallow routing

Shallow routing 是一種用於同一個 page 的路由,你能夠改變 url 上的 query string,但是不執行 getServerSidePropsgetStaticPropsgetInitialProps 裡面的程式,此外還會保留 page 中的狀態。

要使用 shallow routing 需要用到 useRouter() ,在 router.push 的第三個參數加上 { shallow: true }

router.push(`/?counter=123`, undefined, { shallow: true });

以下是一個簡單的範例,點擊「add counter」的按鈕後,會新增一個隨機的數字,同時修改 url 上的 counter - query string,而 useEffect 會監聽 router.query.counter ,在改變時儲存到 counters 狀態中。

import { useState, useEffect } from "react";
import { useRouter } from "next/router";

// 目前 url 為 '/'
function Page() {
  const router = useRouter();
  const [counters, setCounters] = useState<number[]>([]);

  const handleClick = () => {
    const counter = Math.round(Math.random() * 100);
    router.push(`/?counter=${counter}`, undefined, { shallow: true });
  };

  useEffect(() => {
    if (router.query.counter) {
      setCounters((prev) => [
        ...prev,
        parseInt(router.query.counter as string),
      ]);
    }
  }, [router.query.counter]);

  return (
    <div>
      <ul>
        {counters.map((counter) => (
          <li key={counter}>{counter}</li>
        ))}
      </ul>
      <button onClick={handleClick}>add counter</button>
    </div>
  );
}

export default Page;

你可能會踩到 Shallow routing 的雷 ?

Shallow routing 有一個限制是「只能在同一個 url 上切換」,像是以上方的範例來說,我們在在 url 上加上一段 query string — /?counter=${counter} ,這樣的操作是合法的。以下示範一個不合法的操作:

router.push(`/products?counter=${counter}`, undefined, { shallow: true });

這個操作會將目前的頁面轉移到 /product,等於 { shallow: true } 變成是一段沒有用的參數。

Reference


上一篇
Day04 - Next.js 的 file-based routing
下一篇
Day06 - 用 Next.js 做一個簡易產品介紹頁,使用 file-based routing
系列文
從零開始學習 Next.js30

尚未有邦友留言

立即登入留言