iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 11
3
Modern Web

從 Hooks 開始,讓你的網頁 React 起來系列 第 11

[Day 11 - 網速轉換器] 那個...資料可以分享給我嗎 - 將資料傳入組件

昨天的網速單位轉換器仍然還是半成品,因為 <CardFooter /> 組件的樣式雖然可以透過我們手動修改 inputValue 的值而有變化,但是它還沒辦法根據使用者輸入的內容,自動變換這樣些樣式。今天就讓我們來看一下要怎麼在組件之間傳遞和修改資料。

以下是今天會涵蓋到的內容:

  • 將資料從父層組件(parent component)傳入子層組件(child component)

將資料從父層組件傳遞到子層組件

開始前你可以先打開昨天完成的 CodePen,或從 Day 10 - Network Speed Converter with multiple components 複製一份出來。

從下圖中可以看到,inputValue 可以取得使用者在對話框中輸入的內容,但 inputValue 是存在於 <SpeedConverter /> 這個父層組件的資料狀態,現在為了要讓 <CardFooter /> 可以根據 inputValue 的值而改變樣式,我們必須要把 inputValue 的值傳到 <CardFooter />

Imgur

在 React 中把父層組件的資料狀態傳遞到子層組件的方式非常簡單,只需要透過類似 HTML 屬性的方式放在該組件的標籤內就可以了,接著在子層組件的參數中,就可以透過 props 把傳入的資料取出,像是這樣:

// STEP 2: 在該 component 內可以透過參數 props 取得傳入的資料
function ChildComponent(props) {
  return <h1>Hello, {props.firstName} {props.lastName}</h1>;    // Hello, Aaron Chen
}

// STEP 1: 將資料透過 html 屬性的方式傳入 component 內
const element = <ChildComponent firstName="Aaron" lastName="Chen" />;

在取用 props 的時候,會習慣使用解構賦值直接把需要的變數取出來,因此在取用 props 的地方會像這樣寫:

// 透過解構賦值把 props 內需要用到的變數取出
function ChildComponent(props) {
  const { firstName, lastName } = props;
  return <h1>Hello, {firstName} {lastName}</h1>;    // Hello, Aaron Chen
}

甚至更精簡到連 props 都不命名了,直接取出來用:

// 透過解構賦值直接在「函式參數的地方」把需要用到的變數取出
function ChildComponent({ firstName, lastName }) {
  return <h1>Hello, {firstName} {lastName}</h1>;    // Hello, Aaron Chen
}

回到網速轉換器的資料傳遞

因此在網速轉換器的這個範例中,我們可以在 <SpeedConverter /> 組件中使用 <CardFooter /> 的地方,把想要傳入的資料透過 <CardFooter key={value} /> 的方式傳入:

const SpeedConverter = () => {
  const [inputValue, setInputValue] = useState(0);

  const handleInputChange = (e) => {
    const { value } = e.target;
    setInputValue(value);
  };

  return (
    <div className="container">
      {/* ... */}
      {/* STEP 1: 把想要傳入 CardFooter 的資料透過 key={value} 的方式傳入 */}
      <CardFooter inputValue={inputValue} />
    </div>
  );
};

提示:在使用 props 傳遞資料時, keyvalue 的命名可以自己取,不需要相同,只是這裡都剛好取做 inputValue

接著在 <CardFooter /> 的組件中,就可以在參數中透過 props 取得傳進來的資料,props 本身會是一個物件,因此一樣可以透過解構賦值的方式,把想要的資料取出:

// STEP 2:透過 props 取得從父層傳入的資料
const CardFooter = (props) => {
  const { inputValue } = props;

  // ...
  return (
    {/* ... */ }
  );
};

整個流程會像這樣:

Imgur

如果你對於 props 還不是這麼熟悉的話,也可以在 <CardFooter /> 中透過 console.log(props) 把它呈現出來看一下。

如此,將可以把使用者在對話框中輸入的內容傳入 <CardFooter /> 內,而 <CardFooter /> 取得 inputValue 後,就可以根據它的值來決定要呈現的樣式:

Imgur

完整的程式碼可以參考 Day 11 - Network Speed Converter with props

為之前實作的計數器加上起始值

為了更熟悉資料傳遞的這個概念,我們來練習把先前完成的計數器加上起始值。還記得我們可以透過使用很多次的 <Counter /> 來重複渲染很多個計數器嗎?現在我們希望畫面上有五個計數器,但每個計數器一開始都有不一樣的起始值,分別就是 1 ~ 5,像下圖這樣,可以怎麼做呢?

Imgur

你可以從這個 Day 8 - Multiple Counters Finished CodePen 開始,想想看可以怎麼做!

把起始值透過 props 傳入組件

首先,我們可以在每一次渲染 <Counter /> 的地方透過 props 帶入起始值。這裡我先把原本用 map 自動產生多個 <Counter /> 的部分註解掉,手動去寫:

ReactDOM.render(
  <div
    style={{
      display: 'flex',
      flexWrap: 'wrap',
    }}
  >
    {/* counters.map(item => <Counter />) */}
    {/* STEP 1:把每一個 <Counter /> 中透過 `startingValue={}` 的方式給入起始值 */}
    <Counter startingValue={1} />
    <Counter startingValue={2} />
    <Counter startingValue={3} />
    <Counter startingValue={4} />
    <Counter startingValue={5} />
  </div>,
  document.getElementById('root')
);

接下來在每一個 <Counter /> 組件內一樣可以透過 props 的方式取得 startingValue,在把這個值放在 useState(<defaultValue>) 中,如此即會變成 count 的預設值,像是這樣:

// STEP 2:透過 props 取得外層傳入的 startingValue
const Counter = (props) => {
  // STEP 3: 使用解構賦值把 startingValue 從 props 中取出
  const { startingValue } = props;

  // STEP 4: 把 startingValue 當作 count 的預設值放入 useState 的參數中
  const [count, setCount] = useState(startingValue);

  const handleIncrement = () => setCount(count + 1);
  const handleDecrement = () => setCount(count - 1);

  return (
   {/* ... */}
  );
};

如此就可以讓每個 <Counter /> 在一開始的時候都帶有不同的起始值。完整的程式碼可以參考 Day 11 - Multiple Counters with different startingValue by props

讓每個計數器都有不同的最大值和最小值

現在如果我們想要讓這 5 個 <Counter /> 也都有各自不同的最大值和最小值的話,你可以想想看該怎麼做嗎?完成的效果會像這樣:

Imgur

實際的做法和上面幾乎雷同,因此就不再贅述,你可以花 5 ~ 10 分鐘自己嘗試看看,完成的程式碼可以參考 Day 11 - Multiple Counters with different min and max number by props @ CodePen。

⚠️ 重要補充:千萬不能在條件式(conditions)、迴圈(loops)或嵌套函式(nested functions)中呼叫 Hook 方法

慣例上所有 React Hooks 的方法都會以 use 作為函式名稱的開頭,例如,useStateuseEffectuseCallback、...等等。現在雖然我們只提到了 useState,但在使用 React Hooks 的方法時有些原則一定要注意。

其中最重要的一個原則是:「不能在條件式(conditions)、迴圈(loops)或嵌套函式(nested functions)中呼叫 Hook 方法」。

什麼意思呢?以 useState 來說,這樣的寫法是正確的:

// ✅ 正確使用
const Counter = () => {
  const [count, setCount] = useState();

  return (
   {/* ... */}
  );
};

但如果因為某些原因而把 useState 放到 if 內時可能會導致嚴重錯誤:

// ❌ 錯誤使用,把 React Hooks 放到 if 內
const Counter = () => {
  if (isValidCounter <= 10) {
    const [count, setCount] = useState();
  }

  return {
    /* ... */
  };
};

要留意的是,以 use 開頭的函式不能放在判斷式內,但像這裡透過 useState() 產出的變數(count)和方法 setCount,則是可以在判斷式內使用的。

之所以會有這樣的規定是因 React 組件(例如,<Counter />)每次在渲染或更新畫面時,都會呼叫產生這個組件的函式(Counter()),而在 React Hooks 中會去記錄這些 Hooks 在函式中被呼叫的順序,以確保資料能夠被相互對應,但若當我們將 Hooks 放到條件式或迴圈時,就會破壞了這些 Hooks 被呼叫到的順序,如此會造成錯誤。

程式範例

參考文章


上一篇
[Day 10 - 網速換算器] 換算起來吧 - 資料綁定與組件拆分
下一篇
[Day 12] 快速了解各組件的資料狀態 - React DevTools
系列文
從 Hooks 開始,讓你的網頁 React 起來30

2 則留言

0
阿展展展
iT邦好手 1 級 ‧ 2019-10-10 04:22:31

資料可以分享給我嗎...
本檔案僅提供學術研究分享 (設計對白 (對不起我去面壁思過

pjchender iT邦新手 5 級 ‧ 2020-03-02 14:58:28 檢舉

低調碼請自行取用...

0
sunbu
iT邦新手 5 級 ‧ 2020-01-31 00:55:10

最後一段最後一行有錯字XD

pjchender iT邦新手 5 級 ‧ 2020-03-02 14:58:04 檢舉

更新好了,非常感謝!

我要留言

立即登入留言