將類似的 UI 或邏輯,變成可重複利用的 React 元件

底下這個 APP 裡,有兩支 JS 檔案都使用幾乎一樣格式和 UI 的卡片,如何讓這個卡片可以重複利用在不同地方,同時卡片裡面又能帶入各自的內容?

這篇學到

  1. props 也能傳遞 HTML 標籤裡的內容
    ex. <h1>我是標題,我也可以被傳遞到別的元件</h1>

  2. 大量的 JSX 可以透過 props.children 方法,就能在別的元件裡讀取。

原本的寫法

類似的卡片框架:上半部有標題、Avatar、開發者名字&連結;下半部是 Icon list。
這個框架在 Popular 和 Results 共重複寫了 2 次,但是兩邊必須帶入各自的資訊。

Popular.js

顯示程式語言排名最高的開發者。

// Popular.js

function ReposGrid ({ repos }) {
  return (
    <ul className='grid space-around'>
      {repos.map((repo, index) => {
        const { owner, html_url, stargazers_count, forks, open_issues } = repo
        const { login, avatar_url } = owner

        return (
          <li key={html_url} className='card bg-light'>

            {/* 重複的上半部 */}
            <h4 className='header-lg center-text'>
              #{index + 1}
            </h4>
            <img
              className='avatar'
              src={avatar_url}
              alt={`Avatar for ${login}`}
            />
            <h2 className='center-text'>
              <a className='link' href={html_url}>{login}</a>
            </h2>

            {/* 重複的下半部 */}
            <ul className='card-list'>
              <li>
                <FaUser color='rgb(255, 191, 116)' size={22} />
                <a href={`https://github.com/${login}`}>
                  {login}
                </a>
              </li>
              <li>
                <FaStar color='rgb(255, 215, 0)' size={22} />
                {stargazers_count.toLocaleString()} stars
              </li>
              <li>
                <FaCodeBranch color='rgb(129, 195, 245)' size={22} />
                {forks.toLocaleString()} forks
              </li>
              <li>
                <FaExclamationTriangle color='rgb(241, 138, 147)' size={22} />
                {open_issues.toLocaleString()} open issues
              </li>
            </ul>

          </li>
        )
      })}
    </ul>
  )
}

Results.js

輸入兩個 Github 使用者,根據專案的星星和 followers 的數目顯示贏家。

// Results.js
    
export default class Results extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      winner: null,
    }
  }

  render() {
    const { winner } = this.state;

    return (
      <div className='grid space-around container-sm'>

        {/* 重複的上半部 */}
        <div className='card bg-light'>
          <h4 className='header-lg center-text'>
            {winner.score === loser.score ? 'Tie' : 'Winner'}
          </h4>
          <img
            className='avatar'
            src={winner.profile.avatar_url}
            alt={`Avatar for ${winner.profile.login}`}
          />
          <h4 className='center-text'>
            Score: {winner.score.toLocaleString()}
          </h4>
          <h2 className='center-text'>
            <a className='link' href={winner.profile.html_url}>
              {winner.profile.login}
            </a>
          </h2>

          {/* 重複的下半部 */}
          <ul className='card-list'>
            <li>
              <FaUser color='rgb(239, 115, 115)' size={22} />
              {winner.profile.name}
            </li>
            {winner.profile.location && (
              <li>
                <FaCompass color='rgb(144, 115, 255)' size={22} />
                {winner.profile.location}
              </li>
            )}
            {winner.profile.company && (
              <li>
                <FaBriefcase color='#795548' size={22} />
                {winner.profile.company}
              </li>
            )}
            <li>
              <FaUsers color='rgb(129, 195, 245)' size={22} />
              {winner.profile.followers.toLocaleString()} followers
            </li>
            <li>
              <FaUserFriends color='rgb(64, 183, 95)' size={22} />
              {winner.profile.following.toLocaleString()} following
            </li>
          </ul>

        </div>
      </div>
    )
  }
}

建立可重複使用的元件 (Reusable component)

1. 建立 Card component

// Card.js
    
export default function Card ({ header, subheader, avatar, href, name, children }) {
  return(
    <div className="card bg-light">
      {/* 卡片的上半部 */}
      <h4 className='header-lg center-text'>
        {header}
      </h4>
      <img
        className='avatar'
        src={avatar}
        alt={`Avatar for ${name}`}
      />
      {subheader && (
        <h4 className='center-text'>
          {subheader}
        </h4>
      )}
      <h2 className='center-text'>
        <a className='link' href={href}>
          {name}
        </a>
      </h2>

      {/* 卡片的下半部 */}
      {/* <ul className='card-list'> 會透過 props.children 傳遞進來 */}
      {children}
    </div>
  )
}
  • 新增 card.js

    將整個卡片裡的所有內容 <div className='card bg-light'> 額外拉出來,建立成一個獨立元件 <Card>

  • 動態內容透過 props 傳遞

    <h4><img><a> 等標籤裡的內容,因為會置放在兩個不同地方,資訊彼此不同,所以另外把這些動態內容取名 ex. headeravatarhref 等,到時內容會透過 props 傳遞進來。

  • 加上 props.children

    使用 prop.children ,會將 <ul className='card-list'> 裡的整個 icon 列表從別的元件透過 prop.children 傳遞進來。(見下一步驟)

:question: 為什麼 <ul className='card-list'> 沒有寫進 <Card> 的 template 裡?

:left_speech_bubble: 因為這個區塊在兩處內容顯示差異比較大,icon 都不一樣,顯示的訊息也不大相同,你也可以嘗試將所有 <li> 裡的 icon 列表用 props 傳遞,但程式會變的複雜也不好維護。

2. 修改 Popular.js

// Popular.js

import Card from './Card';

function ReposGrid ({ repos }) {
  return (
    <ul className='grid space-around'>
      {repos.map((repo, index) => {
        const { owner, html_url, stargazers_count, forks, open_issues } = repo
        const { login, avatar_url } = owner

        return (
          <li key={html_url}>
            {/* 置入 Card 並將裡頭內容透過 props 傳遞 */}
            <Card
              header={`#${index + 1}`}
              avatar={avatar_url}
              href={html_url}
              name={login}
            >
              {/* card-list 整個移入至 Card 裡面 */}
              <ul className='card-list'>
                <li>
                  <FaUser color='rgb(255, 191, 116)' size={22} />
                  <a href={`https://github.com/${login}`}>
                    {login}
                  </a>
                </li>
                ...
              </ul>
            </Card>
          </li>
        )
      })}
    </ul>
  )
}
  • Import <Card> component

    import Card from './Card';

  • props 傳遞內容

    Card 上半部裡的開發者內容透過 props 傳遞

  • card-list 放在 <Card></Card> 裡面

    將下半部整個 <ul className='card-list'></ul> 放在 <Card> 階層底下,要被 <Card></Card> 包起來。

:question: 整個 card-list 移入 <Card> 階層底下,到時 card.js 要怎麼接收讀取?

:left_speech_bubble: 在 card.js 裡使用 props.children ,任何包在元件 ex. <Card></Card> 裡的小孩都能讀取。

3. 修改 Results.js

和 Popular.js 一樣的修改流程

// Results.js

import Card from './Card';

export default class Results extends React.Component {
  render() {
    return (
      <div className='grid space-around container-sm'>

        {/* 置入 Card 並將裡頭內容透過 props 傳遞 */}
        <Card
          header={winner.score === loser.score ? 'Tie' : 'Winner'}
          subheader={`Score: ${winner.score.toLocaleString()}`}
          avatar={winner.profile.avatar_url}
          href={winner.profile.html_url}
          name={winner.profile.login}
        >
          {/* card-list 整個移入至 Card 裡面 */}
          <ul className='card-list'>
            <li>
              <FaUser color='rgb(239, 115, 115)' size={22} />
              {winner.profile.name}
            </li>
            ...
          </ul>
        </Card>
      </div>
    )
  }
}
  • Import <Card> component

    import Card from './Card';

  • props 傳遞內容

    Card 上半部裡的贏家內容透過 props 傳遞

  • card-list 放在 <Card></Card> 裡面

    將下半部整個 <ul className='card-list'></ul> 放在 <Card> 階層底下,要被 <Card></Card> 包起來。

結果

經過改寫,卡片呈現一切正常。

將兩邊卡片的上半部拆成獨立的元件 <Card> ,內容各自透過 props 傳遞;下半部 icon list 差異度較大,所以在既有的 JS 檔案裡保留整個 JSX 架構,但是包在各自引入的 <Card> 元件裡,之後在 card.js 裡用 props.children 就能讀取兩邊各自的 icon list。


相關資源

1 Like