經過前面的介紹之後,相信大家應該有基礎的理解和認知了,接下來我們一樣透過 Rick and Morty API 來練習如何在 app router 下使用 react server component。
前面起專案的動作我就略過了,有需要的話可以參考前面的文章。
在清乾淨一些 default 之後,我決定這次加個 Navbar 好了,跳轉會比較方便,那麼我一樣習慣放在 components 的資料夾下面,大家也可以照自己喜好做決定,做完大概會像下面這樣:
// src/components/Navbar.tsx
import Link from 'next/link'
import React from 'react'
const Navbar = () => {
return (
<nav className="flex justify-between items-center p-2 min-h-60 bg-blue-400">
<div><Link href={`/`}>home</Link></div>
<ul className="flex items-center">
<li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/charas`}>角色</Link></li>
<li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/locations`}>地點</Link></li>
</ul>
</nav>
)
}
export default Navbar
還記得上一篇講到的 layout 嗎?我們可以把 Navbar 塞到那裡讓巢狀結構中,子層每頁都吃到它,如下:
// src/app/layout.tsx
import Navbar from '@/components/Navbar'
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
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={inter.className}>
<Navbar/>
{children}
</body>
</html>
)
}
那我們一樣先從角色清單功能開始做起,先在 app 資料夾下面新增 charas 的路徑,然後記得這裡是 app router的地盤,要照他的規定使用 page.tsx
來定義頁面喔!不然 index 是吃不到設定的。
// src/app/charas/page.tsx
import React from 'react'
const Charas = async () =>
return (
<div>
charas page
</div>
)
}
export default Charas
現在可以不用再透過 getServerSideProps
來處理 fetch Data,可以直接對 component 下 async
然後 await
你的 fetcher
function,如下:
import React from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Link from 'next/link'
import Image from 'next/image'
const charasFetcher = async (url: string) => {
const res = await fetch(url)
const charasData: RickandmortyCharacterRes = await res.json()
return charasData;
}
let currPage = `https://rickandmortyapi.com/api/character`
const Charas = async () => {
const { info, results } = await charasFetcher(currPage)
return (
<div>
charas page
{results?.map((char) => (
<div className="flex items-center w-full my-2 p-4 shadow-xl rounded-lg" key={char.id}>
<div className="">
<Image src={char.image} alt={char.name} width={100} height={100}/>
</div>
<div>
<p>{char.name}</p>
<Link href={`/characters/${char.id}`}>{char.id}</Link>
</div>
</div>
))}
</div>
)
}
export default Charas
拿到表單以後,你會想接著應該是要處理換頁功能,說好用 useState
要在第一行加上 "use client"
的字串來取用 hooks,但如果你真的直接這麼做的話,Typescript 會提醒你不可以做這種事喔!詳細問題連結
Client components cannot be async functions.
還記得這張表嗎?
What do you need to do? | Server Component | Client Component |
---|---|---|
Fetch data | ✅ | ❌ |
Access backend resources (directly) | ✅ | ❌ |
Keep sensitive information on the server (access tokens, API keys, etc) | ✅ | ❌ |
Keep large dependencies on the server / Reduce client-side JavaScript | ✅ | ❌ |
Add interactivity and event listeners (onClick(), onChange(), etc) | ❌ | ✅ |
Use State and Lifecycle Effects (useState(), useReducer(), useEffect(), etc) | ❌ | ✅ |
Use browser-only APIs | ❌ | ✅ |
Use custom hooks that depend on state, effects, or browser-only APIs | ❌ | ✅ |
Use React Class components | ❌ | ✅ |
以上表格內容取自Next官方文件 |
所以我們應該要把功能做更細部的規劃,並往下傳遞:
// "use client" // 你不能同時用async component和hooks
import React from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Link from 'next/link'
import Image from 'next/image'
// 現在可以不用再透過getServerSideProps來處理 fetch Data
// 可以直接對 component 下 async 然後 await 你的 fetcher
const charasFetcher = async (url: string) => {
const res = await fetch(url)
const charasData: RickandmortyCharacterRes = await res.json()
return charasData;
}
let currPage = `https://rickandmortyapi.com/api/character`
// 你要思考拆出client component
const Charas = async () => {
// 讓這裡只解決first load
const { info, results } = await charasFetcher(currPage)
return (
<div>
charas page
{results?.map((char) => (
<div className="flex items-center w-full my-2 p-4 shadow-xl rounded-lg" key={char.id}>
<div className="">
<Image src={char.image} alt={char.name} width={100} height={100}/>
</div>
<div>
<p>{char.name}</p>
<Link href={`/characters/${char.id}`}>{char.id}</Link>
</div>
</div>
))}
</div>
)
}
export default Charas
讓我們再另外寫一個 client component 來處理需要用到 hook 的部分:
"use client"
import React, { useCallback, useState } from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Image from 'next/image'
import Link from 'next/link'
const CharasList = ({
listInfo
}:{
listInfo: RickandmortyCharacterRes
}) => {
const [pageInfo, setPageInfo] = useState({
pageUrl:'https://rickandmortyapi.com/api/character',
next: listInfo.info.next,
prev: listInfo.info.prev,
loading: false,
curr: 1
});
const [charasList, setCharasList] = useState(listInfo.results);
const pageChange = useCallback((status: string) => {
// console.log(pageInfo);
setPageInfo(pre => ({...pre, loading: true}));
if (status === 'next') {
// console.log(pageInfo);
fetch(pageInfo.next!)
.then((response) => response.json())
.then((response: RickandmortyCharacterRes) => {
const info = response.info
setCharasList(response.results)
setPageInfo(pre => ({
...pre,
next: info.next,
pageUrl: pageInfo.next!,
prev: info.prev,
curr: pre.curr + 1,
loading: false
}))
});
}
if (status === 'prev') {
fetch(pageInfo.prev!)
.then((response) => response.json())
.then((response: RickandmortyCharacterRes) => {
const info = response.info
setCharasList(response.results)
setPageInfo(pre => ({
...pre,
next: info.next,
pageUrl: pageInfo.prev!,
prev: info.prev,
curr: pre.curr - 1,
loading: false
}))
});
}
}, [pageInfo])
return (
<>
<div className="flex justify-center items-center">
<button
className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
onClick={() => pageChange('prev')}
disabled={pageInfo.prev===null || pageInfo.loading} >prev</button>
<div>{pageInfo.curr}</div>
<button
className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 min-w-40"
onClick={() => pageChange('next')}
disabled={pageInfo.next===null || pageInfo.loading}>next</button>
</div>
{charasList?.map((char) => (
<div className="flex items-center w-full my-2 p-4 shadow-xl rounded-lg" key={char.id}>
<div className="">
<Image src={char.image} alt={char.name} width={100} height={100}/>
</div>
<div>
<p>{char.name}</p>
<Link href={`/charas/${char.id}`}>{char.id}</Link>
</div>
</div>
))}
</>
)
}
export default CharasList
然後把原本傳入的資料修改一下:
import React from 'react'
import { RickandmortyCharacterRes } from '../../../typings'
import Link from 'next/link'
import Image from 'next/image'
import CharasList from './CharasList'
const charasFetcher = async (url: string) => {
const res = await fetch(url)
const charasData: RickandmortyCharacterRes = await res.json()
return charasData;
}
let currPage = `https://rickandmortyapi.com/api/character`
// 你要思考拆出client component
const Charas = async () => {
// 這裡只解決first load
const charasRes = await charasFetcher(currPage)
return (
<div>
charas page
<CharasList listInfo={charasRes} />
</div>
)
}
export default Charas
那們今天的練習就差不多到這裡,明天我們來試試 Dynamic Routes 的部分。
恭喜要完賽了~
感謝丹尼大的關注啊!有機會拜讀完你的文章再試試 T3 + tRpc 的實作!
哈哈我也跟你學很多 react~