iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
Modern Web

React 走出新手村 系列 第 13

React 走出新手村-重新整理組件

  • 分享至 

  • xImage
  •  

換個方向

在前面系列的文章,已經幫大家複習並深入了解幾個常用的 hook,接下來的章節,是個人開發經驗與結合前端 design pattern 的概念的經驗分享,所以你如果 hook 還不會用的請參考我之前的文章,那麼我們就開始今天的主題吧!
https://ithelp.ithome.com.tw/upload/images/20230913/20129020nYCsnLbSSE.png

Container & Presentational Pattern

今天要講的是很常見也很入門就會遇到的問題,想像一下,當你的功能越變越大的時候,你會很頻繁地和後端的 server 透過 rest api 溝通索取需要陳列的資料,所以當你要處理的 api 增加之後,你所需要陳列的畫面也就會變得很複雜。

問題

那麼常見新手犯的錯誤就是你的 component 太肥大了,下面我用一個常見的新手範例來做示範:

import React, { useEffect, useState } from 'react'

export interface PokemonItem {
  name: string;
  url: string;
}

export interface PokesState {
  count: number;
  next: string | null;
  previous: string | null;
  results: PokemonItem[]
}

const Foo = () => {
  const [rsData, setRsData] = useState<PokesState>({
    count: 0,
    next: null,
    previous: null,
    results: []
  });
  useEffect(() => {
    if(!rsData.results) {
      const controller = new AbortController();
      fetch(`https://pokeapi.co/api/v2/pokemon`, {
          signal: controller.signal
      })
      .then((response) => response.json())
      .then((data) => {
        setRsData(data)
      });
      return () => controller.abort();
    }
  }, [])

  const changeList = (url: string|null) => {
    if(!url) return;
    fetch(url)
    .then((res) => res.json())
    .then((data) => setRsData(data))
  }
  // 因為不想處理props或覺得麻煩而放在內層,但這樣是不對的
  const BtnGroup = () => {
    return (
      <div className='f-c-c'>
        <button
          className="btn filled"
          disabled={rsData.previous===null}
          onClick={() => changeList(rsData.previous)}
        >prev-page</button>
        <button
          className="btn filled"
          disabled={rsData.next===null}
          onClick={() => changeList(rsData.next)}
        >next-page</button>
      </div>
    )
  } 

  return (
    <div>
      <BtnGroup/>
      {rsData?.results?.map((e) => (
        <div key={e.url}>{e.name}</div>
      ))}
    </div>
  )
}

export default Foo;

不論是哪種理由,盡量避免不必要的套件鑲嵌,也就是在 component 裏面 return 之前,令列其他子組件,而且沒有處理 props 的問題直接渲染,這樣不但會造成維護上的困擾,也會產生組件渲染上重複渲染的問題。

修正後

所以,以上述為例原本的 BtnGroup 應該拆到 Foo 組件之外,透過 props 的機制,傳入對應的 data,如下:

import React, { useEffect, useState } from 'react'

export interface PokemonItem {
  name: string;
  url: string;
}

export interface PokesState {
  count: number;
  next: string | null;
  previous: string | null;
  results: PokemonItem[]
}

// 這裡就是拆分的基本方式,對新手來說要定義這些type問題,是相對有門檻的
// 但站在維運角度來說,是有其必要性的
// 在通常來說,會再分離檔案出來,使其保有彈性可以擴充利用
const BtnGroup = ({prev, next, clickFn}: {prev: string|null; next: string|null; clickFn: (url: string|null) => void}) => {
  return (
    <div className='f-c-c'>
      <button
        className="btn filled"
        disabled={prev===null}
        onClick={() => clickFn(prev)}
      >prev-page</button>
      <button
        className="btn filled"
        disabled={next===null}
        onClick={() => clickFn(next)}
      >next-page</button>
    </div>
  )
} 

const Foo = () => {
  const [rsData, setRsData] = useState<PokesState>({
    count: 0,
    next: null,
    previous: null,
    results: []
  });
  useEffect(() => {
    if(!rsData.results) {
      const controller = new AbortController();
      fetch(`https://pokeapi.co/api/v2/pokemon`, {
          signal: controller.signal
      })
      .then((response) => response.json())
      .then((data) => {
        setRsData(data)
      });
      return () => controller.abort();
    }
  }, [])

  const changeList = (url: string|null) => {
    if(!url) return;
    fetch(url)
    .then((res) => res.json())
    .then((data) => setRsData(data))
  }

  return (
    <div>
      <BtnGroup
        prev={rsData.previous}
        next={rsData.next}
        clickFn={changeList}
      />
      {rsData?.results?.map((e) => (
        <div key={e.url}>{e.name}</div>
      ))}
    </div>
  )
}

export default Foo;

概念核心

有了這樣的概念之後,在後續開發設計架構上,就可以將 component 大致上分為兩類:

  1. 負責交換資料並往下傳遞的組件 (component) 對應— Container Pattern。
  2. 負責接收資料並渲染的組件 (component) 對應— Presentational Pattern。

這樣一來當你的專案出現 bug 的時候,你就能很快地去找到相對問題的組件(component) 進行修正,在初期架構階段也較能有效管理對應功能組件。

這樣的概念就是對應上 Design Pattern 的 Container/Presentational Pattern有興趣的話可以參考這個連結

那麼我們試看看做得更徹底一點,將原本的列表封裝成組件吧!

import React, { memo, useEffect, useState } from 'react'

export interface PokemonItem {
  name: string;
  url: string;
}

export interface PokesState {
  count: number;
  next: string | null;
  previous: string | null;
  results: PokemonItem[]
}

const BtnGroup = ({prev, next, clickFn}: {prev: string|null; next: string|null; clickFn: (url: string|null) => void}) => {
  return (
    <div className='f-c-c'>
      <button
        className="btn filled"
        disabled={prev===null}
        onClick={() => clickFn(prev)}
      >prev-page</button>
      <button
        className="btn filled"
        disabled={next===null}
        onClick={() => clickFn(next)}
      >next-page</button>
    </div>
  )
} 
// 這裡因為複雜度不夠的關係,做得比較陽春一點
const PokeElement = ({ poke }:{poke: PokemonItem}) => {
  return(
    <p>
      {poke.name}
      <a href={poke.url}>link</a>
    </p>
  )
}
// 這裡就回歸到之前講memo的優化部分,有興趣的朋友可以回顧參考
// https://medium.com/@LeeLuciano/react-%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B-memo-1cf1cca55167
const MemoPokeElement = memo(PokeElement)

const Foo = () => {
  const [rsData, setRsData] = useState<PokesState>({
    count: 0,
    next: null,
    previous: null,
    results: []
  });
  useEffect(() => {
    if(!rsData.results) {
      const controller = new AbortController();
      fetch(`https://pokeapi.co/api/v2/pokemon`, {
          signal: controller.signal
      })
      .then((response) => response.json())
      .then((data) => {
        setRsData(data)
      });
      return () => controller.abort();
    }
  }, [])

  const changeList = (url: string|null) => {
    if(!url) return;
    fetch(url)
    .then((res) => res.json())
    .then((data) => setRsData(data))
  }

  return (
    <div>
      <BtnGroup
        prev={rsData.previous}
        next={rsData.next}
        clickFn={changeList}
      />
      {rsData?.results?.map((e) => (
        <React.Fragment key={e.url}>
          <MemoPokeElement poke={e}/>
        </React.Fragment>
      ))}
    </div>
  )
}

export default Foo

總結

只要掌握以上原則,你就會慢慢瞭解到什麼樣的時機應該要整理你的組件(component),在 Code Review 的時候就可以順著這個概念去優化。

走出新手村需要的是思辨的能力,習慣是需要慢慢養成的,在團隊中也是一樣,團隊開發必須大家一起養成習慣,如果要別人也和你一樣養相同的習慣,團隊成員必須要有共同的概念價值,也希望大家可以真正運用在現有的專案當中。

給全新手的大禮包

React基本Hook教學


上一篇
React 走出新手村-自製高效 Context Provider
下一篇
React 走出新手村-高階組件 (H.O.C.)
系列文
React 走出新手村 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言