iT邦幫忙

2023 iThome 鐵人賽

DAY 30
0
Modern Web

React 走出新手村 系列 第 30

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

  • 分享至 

  • xImage
  •  

Dynamic Routes

我們今天接續的做每個角色自己的頁面,來理解 Dynamic Routes 的實作層面。
這部分我們可以先借一下 Next 官方的範例來解說:

export default function Page({ params }: { params: { slug: string } }) {
  return <div>My Post: {params.slug}</div>
}

在這範例中,部落格可以包含以下路由 app/blog/[slug]/page.js,其中 [slug] 是部落格貼文的動態分段,下面的表格應該可以讓大家更快理解:

Route Example URL params
app/blog/[slug]/page.js /blog/a { slug: 'a' }
app/blog/[slug]/page.js /blog/b { slug: 'b' }
app/blog/[slug]/page.js /blog/c { slug: 'c' }

回到實作

那麼我們接著將上面的概念融入我們的功能當中,首先我們在同為 charas 路徑下,新增 [charName] 的資料夾來處理我們的頁面,如下:

// src/app/charas/page.tsx
import React from 'react'

type PageProps = {
  params: {
    charID: string
  }
}

const CharPage = ({params: { charID }}:PageProps) => {
  return (
    <div>
      this is {charID}
    </div>
  )
}

export default CharPage

這樣就能夠取到路徑上的 segment,透過它再去 fetch API 拿取角色資料:

import React from 'react'
import { RickandmortyCharacter } from '../../../../typings'
import Link from 'next/link'

type PageProps = {
  params: {
    charID: string
  }
}

const fetchCharacter = async (id: string) => {
  try{
    const res = await fetch(
      `https://rickandmortyapi.com/api/character/${id}`,
      { next: { revalidate: 60 } } // 這裡保有 ISR 的作法
      );
    if(!res.ok){
      const message = `An error occured: ${res.status}`;
      throw new Error(message);
    }
    const characterInfo: RickandmortyCharacter = await res.json();
    return characterInfo
  } catch(err) {
    console.log(err);
  }
} 

const CharPage = async ({params: { charID }}:PageProps) => {
  const character = await fetchCharacter(charID)

  return (
    <div>
      <h2>here is {charID}</h2>
      <div>
        <p>Name: {character?.name}</p>
        <p>status: {character?.status}</p>
        <p>species: {character?.species}</p>
        <p>gender: {character?.gender}</p>
        <p>origin: {character?.origin.name}</p>
        <p>location: {character?.location.name}</p>
      </div>
      <Link 
        className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
        href={'/charas'}
      >back</Link>
    </div>
  )
}

export default CharPage

客製化 NotFound

那我們來處理找不到 ID 角色時的 Error Page,客製化我們的 Not found,首先在同路下新增not-found.tsx 如下:

import React from 'react'

const NotFound = () => {
  return (
    <div>
      Character is not exist!
      當然,這邊可以弄的更漂亮一點,Demo 請見諒
    </div>
  )
}

export default NotFound

然後我們回到 page.tsx 引入 notFound 來使用,如下:

import React from 'react'
import { RickandmortyCharacter } from '../../../../typings'
import Link from 'next/link'
import { notFound } from 'next/navigation'

type PageProps = {
  params: {
    charID: string
  }
}

const fetchCharacter = async (id: string) => {
  try{
    const res = await fetch(
      `https://rickandmortyapi.com/api/character/${id}`,
      { next: { revalidate: 60 } } // 這裡保有 ISR 的作法
      );
    if(!res.ok){
      const message = `An error occured: ${res.status}`;
      throw new Error(message);
    }
    const characterInfo: RickandmortyCharacter = await res.json();
    return characterInfo
  } catch(err) {
    console.log(err);
  }
} 

const CharPage = async ({params: { charID }}:PageProps) => {
  const character = await fetchCharacter(charID);

  if (!character) notFound();

  return (
    <div>
      <h2>here is {charID}</h2>
      <div>
        <p>Name: {character.name}</p>
        <p>status: {character.status}</p>
        <p>species: {character.species}</p>
        <p>gender: {character.gender}</p>
        <p>origin: {character.origin.name}</p>
        <p>location: {character.location.name}</p>
      </div>
      <Link 
        className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
        href={'/charas'}
      >back</Link>
    </div>
  )
}

export default CharPage

先產出頁面

這時候如果角色 ID 找不到就會看到我們客製化的頁面了,既然保留 ISR 的操作,那麼要預先生成頁面也是沒有問題的吧!

沒錯!透過 generateStaticParams 可以讓你預先生成第一頁的畫面資料,如下:

import React from 'react'
import { RickandmortyCharacter, RickandmortyCharacterRes } from '../../../../typings'
import Link from 'next/link'
import { notFound } from 'next/navigation'

type PageProps = {
  params: {
    charID: string
  }
}

const fetchCharacter = async (id: string) => {
  try{
    const res = await fetch(
      `https://rickandmortyapi.com/api/character/${id}`,
      { next: { revalidate: 60 } } // 這裡保有 ISR 的作法
      );
    if(!res.ok){
      const message = `An error occured: ${res.status}`;
      throw new Error(message);
    }
    const characterInfo: RickandmortyCharacter = await res.json();
    return characterInfo
  } catch(err) {
    console.log(err);
  }
} 

const CharPage = async ({params: { charID }}:PageProps) => {
  const character = await fetchCharacter(charID);

  if (!character) notFound();

  return (
    <div>
      <h2>here is {charID}</h2>
      <div>
        <p>Name: {character.name}</p>
        <p>status: {character.status}</p>
        <p>species: {character.species}</p>
        <p>gender: {character.gender}</p>
        <p>origin: {character.origin.name}</p>
        <p>location: {character.location.name}</p>
      </div>
      <Link 
        className="p-2 mx-2 rounded-lg shadow-md bg-blue-500 hover:bg-blue-300 disabled:bg-gray-100 min-w-40"
        href={'/charas'}
      >back</Link>
    </div>
  )
}

export default CharPage

// 這邊為了預先產出靜態檔案而做的處理
export const generateStaticParams = async () => {
  const res = await fetch('https://rickandmortyapi.com/api/character');
  const listData: RickandmortyCharacterRes = await res.json();
  return listData.results.map((character) => ({charID: character.id}))
}

我們來嘗試多練習差不多概念的搜尋功能,邏輯是透過 input 蒐集輸入的名字參數,送出時利用 Rick and Morty API 的 filter 來實現快速搜尋角色的功能,結構上仍是利用 Dynamic Routes 的概念,那我們先在 NavBar 下新增搜尋頁面如下:

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={`/searchCharas`}>搜尋角色</Link></li>
        <li className="mx-4 bg-white rounded-lg p-2 shadow-sm"><Link href={`/locations`}>地點</Link></li>
      </ul>
    </nav>
  )
}

export default Navbar

既然路徑開出來了,我們就接著把 page 生出來吧!

// src/app/searchCharas
import React from 'react'

const SearchCharas = () => {
  return (
    <div>
      search page
    </div>
  )
}

export default SearchCharas

這次我們來練習透過 layout.tsx 來調整路徑下的頁面,那我們一樣在 /searchCharas 下面把檔案生出來如下:

import React from 'react'
import Search from './Search'

const SearchCharasLayout = ({
  children,
}: {
  children: React.ReactNode,
}) => {
  return (
    <main className="flex space-x-4 divide-x-2 p-5">
      <div>
        <h1>Search Characters</h1>
        <Search/>
      </div>
      <div className="flex-1 pl-5">
        <div>{children}</div>
      </div>
    </main>
  )
}

export default SearchCharasLayout

這樣 Search 就不會因為路由的關係被清掉了,那麼我們接著處理 Search component,如下:

"use client";

import { useRouter } from 'next/navigation'
import { FormEvent, useRef } from 'react'

const Search = () => {
  const searchTerm = useRef<HTMLInputElement>(null);
  const router = useRouter();

  const handleSearch = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const str = searchTerm.current
    router.push(`/searchCharas/${str?.value}`)
  }
  return (
    <form onSubmit={handleSearch}>
      <div>
        <input 
          className='p-2 border-b-2 border-gary-200 mx-2'
          type="text" 
          placeholder="Insert character's name"
          ref={searchTerm}
        />
      </div>
      <button
        type='submit'
        className='bg-blue-500 text-white font-bold py-2 px-4 rounded-lg my-2'
      >
        Search
      </button>
    </form>
  )
}

export default Search

這邊就如同先前文章的操作,是轉成 client component 應該很好懂,簡單來說就是在這路徑下再新增一個 Dynamic Route,那麼我們就先把資料夾生出來,這次我讓參數叫做 searchTerm,除了 pagenot-found 以外我多處理了一個 loading 來讓頻繁搜尋的時候不要那麼醜(這邊只是Demo,大家可以自行發揮美感處理),那麼如下:
page.tsx

import React from 'react'
import { RickandmortyCharacterRes } from '../../../../typings';
import { notFound } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';

type PageProps = {
  params: {
    searchTerm: string,
  },
}

const searchCharacters = async (params: string) => {
  try{
    const res = await fetch(
      `https://rickandmortyapi.com/api/character/?name=${params}`,
      { next: { revalidate: 60 } } 
      );
    if(!res.ok){
      const message = `An error occured: ${res.status}`;
      throw new Error(message);
    }
    const characterInfo: RickandmortyCharacterRes = await res.json();
    return characterInfo
  } catch(err) {
    console.log(err);
  }
}

const SearchResults = async ({params: { searchTerm }}: PageProps) => {
  const searchResult = await searchCharacters(searchTerm) 
  if(!searchResult) return notFound();
  return (
    <div>
      {searchResult.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={`/charas/${char.id}`}>{char.id}</Link>
          </div>
        </div>
      ))}
    </div>
  )
}

export default SearchResults

not-found.tsx

import React from 'react'

const NotFound = () => {
  return (
    <div>
      Whoops... we can not find the character.
    </div>
  )
}

export default NotFound

loading.tsx

import React from 'react'

const Loading = () => {
  return (
    <div>
      loading character...
    </div>
  )
}

export default Loading

那麼,你的列表現在就多出了一個搜尋的功能了!

總結

今天的內容我相信也差不多了,我會把範例留在我的 GitHub 連結裏面,大家在練習的時候可以再想想怎麼讓其他功能也更加完善。

給全新手的大禮包

React基本Hook教學


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

尚未有邦友留言

立即登入留言