iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 10
6
Modern Web

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

[Day 10 - 網速換算器] 換算起來吧 - 資料綁定與組件拆分

  • 分享至 

  • xImage
  •  

感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。

在 React 18 後已經棄用 ReactDOM.render(),改用 ReactDOM.createRoot(),內文中的圖片並未一併修改,煩請讀者留意。

昨天我們已經把版哥提供的網速轉換器 HTML 樣板,搬移到 JavaScript 中改成在 React 中用 JSX 來呈現,但是還沒加上任何實際的功能。

今天主要會涵蓋到的內容包含:

  • [複習] setState 的使用
  • [複習] 條件轉譯
  • [New] 綁定資料在表單元素上
  • [New] 將 JSX 拆成多個元件

整個操作流程將會像這樣子:

  • 輸入 Mbps 自動轉換成 MB/s
  • 根據輸入的數字不同,最下方會呈現不同的樣式和文字說明

Imgur

現在就讓我們把功能加進去!你可以先打開昨天完成的 CodePen 或者到 Day 9 - Network Speed Converter - Started Template 按下 Fork 來修改。

取得使用者輸入的資料

首先要能夠將 Mbps 轉換成 MB/s 之前, ㄧ定需要先知道使用者輸入了什麼內容,因此在網速單位轉換器中,在 .converter 區塊內有 <input /> 標籤是要讓使用者輸入的:

const SpeedConverter = () => {
  return (
    {/* ... */}
    <div className="card-body">
      <div className="converter">
        <div className="flex-1">
          <div className="converter-title">Set</div>
          {/* ⚠️ 取得使用者輸入的內容 */}
          <input type="number" className="input-number" min="0" />
        </div>
        {/* ... */}
      </div>
    </div>
    {/* ... */}
  );
};

綁定 onChange 事件

和之前學到綁定點擊事件的方式相同,若要監控使用者在 <input /> 欄位中輸入了什麼,可以使用 onChange 事件,可以在 <input /> 內加上 onChange,在後面的 {} 內透過 console.log 來看看是否會觸發此事件,像是這樣:

{
  /* ... */
}
<div className="flex-1">
  <div className="converter-title">Set</div>
  <input
    type="number"
    onChange={() => console.log('onChange')}
    className="input-number"
    min="0"
  />
</div>;
{
  /* ... */
}

Imgur

現在當使用者在對話框中輸入數字時,應該就可以在瀏覽器的 console 視窗中看到一直會跳出 onChange 的訊息:

Imgur

透過 useState 讓 React 明白資料的變化

現在雖然在使用者輸入對話框的訊息時,會觸發 onChange 事件,但和 Day 06 - 計數器動起來吧 - useState 的使用 中曾提過的「為什麼數字不動」一樣,只是監聽事件 React 並沒辦法得知它內部是否有任何資料改變,因此它也不知道是不是需要重新轉譯畫面。

為了要讓使用者在輸入左側的 Mbps 時,右側 MB/s 在畫面上的值能夠連動改變,因此需要把使用者輸入的內容透過 state 紀錄在 React 元件中,一但 React 發現這個 state 的內容有改變時,就會重新轉譯畫面。要在 React 元件中紀錄資料,就會用到曾在 [Day 06] 計數器動起來吧 - useState 的使用 用過 useState 這個 React Hooks。

現在我們要做的就是讓使用者在左側輸入資料後,直接把同樣的內容呈現在右側(先不用作單位換算),像是這樣:

Imgur

不囉唆,上扣!

// STEP 1:取出 useState 這個方法
const { useState } = React;

const SpeedConverter = () => {
  // STEP 2: 定義 state,取得預設值為 0 的 inputValue 和修改該狀態的 setInputValue 方法
  const [inputValue, setInputValue] = useState(0);

  // STEP 3: 定義事件處理器
  const handleInputChange = (e) => {
    // STEP 3-1: 取出使用者輸入的值
    const { value } = e.target;

    // STEP 3-2: 將這個值設定到 state 中
    setInputValue(value);
  };

  return (
    <div className="container">
      <div className="card-header">Network Speed Converter</div>
      <div className="card-body">
        <div className="unit-control">
          <div className="unit">Mbps</div>
          <span className="exchange-icon fa-fw fa-stack">
            <i className="far fa-circle fa-stack-2x" />
            <i className="fas fa-exchange-alt fa-stack-1x" />
          </span>
          <div className="unit">MB/s</div>
        </div>
        <div className="converter">
          <div className="flex-1">
            <div className="converter-title">Set</div>
            {/* STEP 4: 把事件處理器綁定進去,並且把 value 帶入 */}
            <input
              type="number"
              onChange={handleInputChange}
              value={inputValue}
              className="input-number"
              min="0"
            />
          </div>
          <span
            className="angle-icon fa-2x"
            style={{
              marginTop: 30,
            }}
          >
            <i className="fas fa-angle-right" />
          </span>
          <div className="text-right flex-1">
            <div className="converter-title">Show</div>
            {/* STEP 5: 把使用者輸入的值顯示於畫面上 */}
            <input
              className="input-number text-right"
              type="text"
              value={inputValue}
              disabled
            />
          </div>
        </div>
      </div>
      <div className="card-footer">FAST</div>
    </div>
  );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<SpeedConverter />);

程式的改變主要分成 5 個部分:

  • Step 1:從 React 中取出 useState 這個方法
  • Step 2:使用 useState() 定義 inputValue 這個資料狀態(state)預設值為 0,並取得更新狀態的 setInputValue 這個方法
  • Step 3:定義事件處理器(handleInputChange),透過 e.target.value 可以取得使用者輸入的內容,這裡透過解構賦值的方式把 value 取出來,接著透過 setInputValue 更新在 React 中資料狀態
  • Step 4:把 handleInputChange 放到 onChange 的事件處理器中;把對話框 value 的值等同於 React 中 inputValue 的值
  • Step 5:透過 value={inputValue} 把使用者輸入的 inputValue 顯示於畫面上。

提示:在 React 中,常使用 handle 當作事件處理器的開頭,例如 onClick 對應到 handleClickonChange 對應到 handleChange

這裡比較需要留意的是,使用者之所以能夠在畫面的右側看到自己輸入的內容,是因為下面這一連串過程導致畫面重新轉譯後,才把最新的 inputValue 顯示在使用者的畫面上:

Imgur

進行網速單位換算

昨天的內容中 Day 09 -網速傻傻分不清楚 Mbps? MB/s? 來寫個單位換算器吧 有提到 1 Mbps = 0.125 MB/s ,也就是 Mbps 的值除以 8 才會是 MB/s

Imgur

因此,要正確的單位轉換,只需要修改右側帶入的 <input /> 中帶入的 value,讓它是使用者輸入的值除以 8 即可。

把上面 STEP 5 的部分改成:

{
  /* STEP 5: 把使用者輸入的值顯示於畫面上 */
}
<input
  className="input-number text-right"
  type="text"
  value={inputValue / 8}
  disabled
/>;

就可以了。

這個部分完整的程式碼可以參考 Day 10 - Network Speed Converter - useState @ CodePen。

拼積木嘍 - React 元件的拆分

最後,我們希望畫面最下方的內容可以隨著使用者輸入的數值不同而呈現不同的顏色:

Imgur

在開始使用條件轉譯之前,我們要先來說明在 React 中元件拆分的概念。

先前,我們在整個頁面中都只使用一個 React 元件,在這個元件中包含所有的 HTML 結構,但是當專案一大起來之後,一個頁面中若包含所有的 HTML 結構、邏輯判斷等等,將會變得難以管理,你可以想像一隻檔案打開來後有好幾萬行,要如何找到想要修改的元素呢?

因此在 React 中,元件除了有前面 Day 08 - 一個不夠,給我一次來十個 - JSX 中迴圈的使用 提到的,可以重複使用外,還有一點是方便我們去管理各個「功能獨立」的元素。之後,我們會再把每個元件都拆分成的路獨立的 JS 檔案,這樣管理上就會方便許多了。

React 元件的拆分非常簡單,其實你早就會了。舉例來說,現在想要把 <div className="unit-control">...</div> 的這個區塊拆成一個獨立的元件:

Imgur

只需要透過函式定義一個新的 React 元件,名稱就取為 UnitControl,要留意的是 React 元件的命名是使用大寫駝峰,因此首字需要大寫,同時因為這個元件單純只是要回傳 JSX 而沒有要做其他處理,所以可以在箭頭函式的 => 後直接回傳 JSX 即可,像這樣:

const UnitControl = () => (
  <div className="unit-control">
    <div className="unit">Mbps</div>
    <span className="exchange-icon fa-fw fa-stack">
      <i className="far fa-circle fa-stack-2x" />
      <i className="fas fa-exchange-alt fa-stack-1x" />
    </span>
    <div className="unit">MB/s</div>
  </div>
);

這樣就完成了一個名為 <UnitControl /> 的 React 元件,只需要在你想要使用它的地方帶入即可:

Imgur

除了 <UnitControl /> 可以拆分成獨立的元件外,現在,我們要根據使用者輸入的值不同來呈現不同的畫面,因為只會改變到 .card-footer 的部分,為了不要把判斷的邏輯也都全部寫在 SpeedConverter 內,所以一樣可以把 <div class="card-footer">...</div> 的部分拆成一個獨立的元件,在裡面在使用條件判斷來決定要呈現的樣式:

Imgur

到目前為止完整的程式碼會像這樣:

const { useState } = React;

const UnitControl = () => (
  <div className="unit-control">
    <div className="unit">Mbps</div>
    <span className="exchange-icon fa-fw fa-stack">
      <i className="far fa-circle fa-stack-2x" />
      <i className="fas fa-exchange-alt fa-stack-1x" />
    </span>
    <div className="unit">MB/s</div>
  </div>
);

const CardFooter = () => {
  return <div className="card-footer">FAST</div>;
};

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

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

  return (
    <div className="container">
      <div className="card-header">Network Speed Converter</div>
      <div className="card-body">
        <UnitControl />
        <div className="converter">
          <div className="flex-1">
            <div className="converter-title">Set</div>
            <input
              type="number"
              onChange={handleInputChange}
              value={inputValue}
              className="input-number"
              min="0"
            />
          </div>
          <span
            className="angle-icon fa-2x"
            style={{
              marginTop: 30,
            }}
          >
            <i className="fas fa-angle-right" />
          </span>
          <div className="text-right flex-1">
            <div className="converter-title">Show</div>
            <input
              className="input-number text-right"
              type="text"
              value={inputValue / 8}
              disabled
            />
          </div>
        </div>
      </div>
      <CardFooter />
    </div>
  );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<SpeedConverter />);

現在,這個頁面中就同時有三個 React 元件,分別是 <SpeedConverter /><UnitControl />,和 <CardFooter />

條件轉譯的使用

現在,我們只需要專注在 <CardFooter /> 這個元件上就好,在 <CardFooter /> 中,先定義好不同網速時要顯示的背景顏色和文字內容,並放到名為 criteria 的變數中,像是這樣:

const CardFooter = () => {
  // STEP 1:inputValue 是使用者輸入的數值,暫時先設成 30
  let inputValue = 30;
  // STEP 2:定義 criteria 物件
  let criteria;

  // STEP 3:根據 inputValue 改變要顯示的內容和背景色
  if (!inputValue) {
    criteria = {
      title: '---',
      backgroundColor: '#d3d8e2',
    };
  } else if (inputValue < 15) {
    criteria = {
      title: 'SLOW',
      backgroundColor: '#ee362d',
    };
  } else if (inputValue < 40) {
    criteria = {
      title: 'GOOD',
      backgroundColor: '#1b82f1',
    };
  } else if (inputValue >= 40) {
    criteria = {
      title: 'FAST',
      backgroundColor: '#13d569',
    };
  }

  return <div className="card-footer">Fast</div>;
};

接著在最後 return 的地方,把對應的背景顏色和標題帶進去就可以了:

// STEP 4:把定義好的 criteria 物件帶入 JSX 內
return (
  <div
    className="card-footer"
    style={{
      backgroundColor: criteria.backgroundColor,
    }}
  >
    {criteria.title}
  </div>
);

現在,你應該可以看到畫面,同時可以試著去改變 inputValue 來測試看看畫面內容是否會改變:

Imgur

到現在,我們可以透過手動修改在 <CardFooter /> 中的 inputValue 來讓最下方的區塊改變,但因為在 <CardFooter /> 中還沒辦法取得使用者實際上在對話框中輸入的 inputValue,因此還無法讓它自動根據使用者在對話框中輸入的值來自動變化。

要如何將外層 <SpeedConverter />inputValue 的狀態傳遞到 <CardFooter /> 內,會需要談到怎麼樣在 React 的元件間傳遞資料,這麼部分就讓我們留到明天再繼續說明。

今天完成的程式碼可以到 Day 10 - Network Speed Converter with multiple components @ CodePen 檢視。

程式範例

參考文章

  • Forms @ React Docs - main concepts

上一篇
[Day 09 - 網速換算器] 網速傻傻分不清楚 Mbps? MB/s? 來寫個單位換算器吧
下一篇
[Day 11 - 網速轉換器] 那個...資料可以分享給我嗎 - 將資料傳入組件
系列文
從 Hooks 開始,讓你的網頁 React 起來30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ert60213
iT邦新手 5 級 ‧ 2020-01-29 22:02:23

不好意思,想請教一個問題
https://ithelp.ithome.com.tw/upload/images/20200129/20124487BqUVZzEy4C.jpg
Step 3:定義事件處理器(handleInputChange)
這裡透過e.target.value是如何取得使用者輸入的內容呢
是慣用用法嗎,主要不理解
const { value } = e.target;
這邊解構賦值大括弧的value,是怎麼取得使用者輸入的數值?
麻煩了,謝謝!

pjchender iT邦新手 3 級 ‧ 2020-01-29 23:12:34 檢舉

在表單元素的事件處理器中,都可以接收到 e (event) ,透過 e.target.value 就可以取得使用者輸入的資料,一般比較常看到寫法可能是:

const text = e.target.value;

在這裡其實是一樣的,只是改成用解構賦值的寫法:

const value = e.target.value;

// 變成直接把 e.target 這個物件中的 value 取出
// 並同時建立一個名為 value 的變數
const { value } = e.target;

不知道這是不是你想問的呢?

ert60213 iT邦新手 5 級 ‧ 2020-01-30 14:08:54 檢舉

不好意思想問更細一點

const value = e.target;
// e.target 這個物件本身就內含value嗎
pjchender iT邦新手 3 級 ‧ 2020-03-02 14:49:46 檢舉

ert60213 是的,這個物件內本身就含有 value。

我要留言

立即登入留言