在了解了 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.tsx
或 pages/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
都能夠接受 string
或 UrlObject
兩種定義路由的方式,當然也能夠接受動態生成 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 便無法順利的編譯。
在許多情況下 <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 是一種用於同一個 page 的路由,你能夠改變 url 上的 query string,但是不執行 getServerSideProps
、 getStaticProps
與 getInitialProps
裡面的程式,此外還會保留 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 有一個限制是「只能在同一個 url 上切換」,像是以上方的範例來說,我們在在 url 上加上一段 query string — /?counter=${counter}
,這樣的操作是合法的。以下示範一個不合法的操作:
router.push(`/products?counter=${counter}`, undefined, { shallow: true });
這個操作會將目前的頁面轉移到 /product
,等於 { shallow: true }
變成是一段沒有用的參數。