iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Modern Web

React 走出新手村 系列 第 29

React 走出新手村 — Rick and Morty練習(I)

  • 分享至 

  • xImage
  •  

實作練習

經過前面的介紹之後,相信大家應該有基礎的理解和認知了,接下來我們一樣透過 Rick and Morty API 來練習如何在 app router 下使用 react server component。

前面起專案的動作我就略過了,有需要的話可以參考前面的文章。

加個Navbar

在清乾淨一些 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 的部分。

給全新手的大禮包

React基本Hook教學


上一篇
React 走出新手村 — Next App Router
下一篇
React 走出新手村 — Rick and Morty練習(II)
系列文
React 走出新手村 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
danny101201
iT邦新手 3 級 ‧ 2023-09-29 22:55:24

恭喜要完賽了~

LucianoLee iT邦研究生 5 級 ‧ 2023-09-30 00:07:13 檢舉

感謝丹尼大的關注啊!有機會拜讀完你的文章再試試 T3 + tRpc 的實作!

哈哈我也跟你學很多 react~

我要留言

立即登入留言