iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 6
10
Modern Web

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

[Day 06 - 計數器] 醒醒啊!為什麼一動也不動 - useState 的基本使用

  • 分享至 

  • xImage
  •  

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

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

昨天我們已經把計數器的 HTML 放到 JavaScript 中,並且用 JSX 的方式來呈現,但是目前只有畫面還沒有實際的功能。今天我們將會使用第一個 React Hooks,主要的重點會包含:

  • 在 JSX 中綁定事件監聽器
  • 使用 useState 來定義和改變資料狀態
  • React 只會重新轉譯畫面中有變更的部分

現在就讓我們開始,讓計數器動起來吧!

Day 04 - 把 HTML 寫在 JavaScript 中有什麼好處? 的內容中,我們曾經透過 React 官網提供的 Hello World 樣板 來說明如何把變數放到 JSX 中。現在當我們把 JSX 包在一個 React Component 中時,若想要帶入變數可以怎麼做呢?

在 React 元件中使用變數

做法其實是一樣的,因為 React Component 就是一個會回傳 JSX 的 JavaScript 函式。現在,我們先把昨天完成的 <Counter /> 內的數字部分改成用變數來呈現,你可以 Fork 一份昨天在 CodePen 的範例(Day 5 - Counter with React Component)來開始今天的練習。

昨天因為這個 <Counter /> 元件是直接回傳一個 JSX,所以在箭頭函式中,我們可以在 => 沒有寫 return 的情況下就填入要回傳的內容,像是這樣:

const Counter = () => (
  <div className="container">
    <div className="chevron chevron-up" />
    <div className="number">256</div>
    <div className="chevron chevron-down" />
  </div>
);

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

但現在因為我們要在函式內加入變數,所以要改回最一般箭頭函式的寫法,也就是 () => {},這時候就可以在這個函式中加入計數器的變數,像是這樣:

const Counter = () => {
  const count = 256;

  return (
    <div className="container">
      <div className="chevron chevron-up" />
      <div className="number">{count}</div>
      <div className="chevron chevron-down" />
    </div>
  );
};

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

可以看到我們在 Counter 函式的最上面先定義名為 count 的變數,接著把變數帶入 JSX 的方式完全一樣,使用 {} 就可以了:

Imgur

既然數字已經是個變數,代表我們可以去修改它的值,你可以把它改成 <div className="number">{count * 2}</div>,那個最後畫面出現的數字就會是 512

補充:在 JSX 中因為它本質上是 JavaScript,所以如果想在 JSX 內撰寫註解的話,可以把註解寫在 {} 裡面,像是這樣 {/* 這裡是註解 */}

在 React 元件中綁定事件監聽器

為了讓計數器能夠運作,現在我們需要一個方法來改變 count 這個變數。先前使用原生的 JavaScript 時,我們是使用 addEventListener('click', ...) 的方法,在 React 元件中則是會透過 onClick 直接把事件綁定在 JSX 上面。例如現在想要在「向上箭頭」的按鈕綁定點擊事件時,可以在 JSX 中這樣寫:

<div className="chevron chevron-up" onClick={/* ... */} />

onClick={...}{...} 內要放的就是點擊後要做什麼處理, 一般稱作事件處理器(event handlers),它會是一個函式。我們來試試看,當使用者點擊向上箭頭的時候,在 console 中顯示訊息,像是這樣:

const Counter = () => {
  const count = 256;

  return (
    <div className="container">
      {/* 監聽 onClick 事件,並放入事件處理函式 */}
      <div
        className="chevron chevron-up"
        onClick={() => {
          console.log(`current Count is ${count}`);
        }}
      />
      <div className="number">{count * 2}</div>
      <div className="chevron chevron-down" />
    </div>
  );
};

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

修改的部份如下:

Imgur

效果則會像這樣,當點擊向上箭頭時會出現在 console 視窗中跳出訊息,而向下箭頭因為還沒有註冊事件,因此點擊後不會有反應:

Imgur

提示:還記得 JSX 是一種強化版的 HTML,然後寫在 JSX 中的 HTML 屬性會使用小寫駝峰的方式來命名。如果你想要在 JSX 中綁定其他事件的屬性名稱,可以參考 React 官網中關於 Events 的 API 資料。

數字為什麼就是不動(怒

接下來可能會很直覺的想說,既然現在已經可以監聽使用者的點擊事件,那要改變數字就沒問題了,只需要像先前原生 JavaScript 的寫法一樣,在使用者點擊畫面時,把 count + 1 就可以了吧!?

於是我們把 const count = 256 改成 let count = 256count 可以重新被賦值,在 onClick 的時候讓這個 count 變數的值加 1,程式碼會像這樣:

const Counter = () => {
  let count = 256; // 定義變數

  return (
    <div className="container">
      <div
        className="chevron chevron-up"
        onClick={() => {
          count = count + 1;
          console.log(`current Count is ${count}`);
        }}
      />
      <div className="number">{count}</div>
      <div className="chevron chevron-down" />
    </div>
  );
};

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

程式碼改變的部份如下:

Imgur

實測的結果(程式碼連結)和我們預期的卻不太一樣...,你可以看到當我們點擊下的箭頭時,雖然右邊 console 顯示的 count 數字有持續增加,但是畫面的數字卻是變也不變!為什麼會這樣子呢?

Imgur

這個部分的程式碼可以參考 CodePen 連結 Day 6 - Counter with variable and event handlers

一開始碰到這個問題會有些困惑,但了解之後原因並不難懂。因為雖然 count 的數字更新了,但 React 並不知道數字有更新,所以它不會去重新轉譯瀏覽器的畫面,這個感覺有點類似先前在 Day 03 中使用原生 JavaScript 撰寫計數器時,雖然在使用者點擊按鍵後有把 count + 1 ,但最後沒有使用 numberElement.textContent 把更新後的值重新給回網頁的情況。

當然,我們不能只是說說而已,要確定是不是真的因為 React 元件的畫面沒有更新(重新轉譯)才使得畫面上的數字沒有改變的話,我們可以在 JSX 中使用 {console.log('render')} 來看看,像是這樣:

由於 一個 JSX 元素只能有一個根節點 ,也就是說最外層一定只會一個 HTML 標籤或 React 元件把大家都包住,因此 {console.log('render')} 這段記得要寫在 <div className="container"> 內。

關於一個 JSX 元素只能有一個根節點的這個部分會在後面做更多說明。

你會看到,我們的畫面只有被轉譯了一次,即使之後 count 變數更新了,畫面也沒有跟著更新(重新轉譯):

Imgur

所以現在重點就是要讓 React 知道,我們的數字改變了,並請它幫我們更新畫面。

透過 useState 讓 React 知道有東西變了

好在 React 提供了方法可以來監控並改變這些資料,一旦使用 React 中提供的方法來修改資料時,React 一發現到資料內容有變動時,就會自動更新畫面,而這個方法就是這裡要提到的第一個 React Hooks - useState

這個方法之所以叫做 useState 是因為在 React 元件中,這些會連動導致畫面改變的「資料(data)」習慣上被稱作「狀態(state)」。以紅綠燈來說,假設有一個資料可以用來表示紅綠燈的顏色,0 是紅燈、1 是綠燈,當這個資料是 0 的時候,燈號就會變成紅燈的「狀態」;當資料變成 1 時,燈號就會變成「綠燈」的狀態。

因此當你以後聽到開發者在討論某個元件的「狀態」時,通常不是指元件有沒有生病或依然健在的那個狀態,而是在說現在的「資料」是長什麼樣子。

在 React 中講到「狀態(state)」時,一般你可以直接把它成「資料(data)」來理解。

至於 useState 的方法前面之所以會多了個 use ,是因為這是在 React Hooks 中的慣例,只要開頭為 use 的函式,就表示它是個 "Hook"。先讓我們來看一下怎麼使用 useState 這個 React Hooks,之後再來對 Hooks 做更多的說明。

先實作在解釋

我們先來直接實作,過程中可能會有一些你不太了解的程式碼沒關係,先做出效果來,後面會再做說明。過程主要包含四個步驟:

  • STEP 1:從 React 物件中取出 useState 方法
  • STEP 2:呼叫 useState 方法後可以取得一個「變數(count)」和「改變該變數的方法(setCount)」
  • STEP 3:在使用者點擊向上箭頭時,透過 setCount 方法將變數 count 加 1
  • STEP 4:在使用者點擊向下箭頭時,透過 setCount 方法將變數 count 減 1
// STEP 1: 從 React 物件中取出 `useState` 方法
const { useState } = React;

const Counter = () => {
  // STEP 2:
  // 透過 useState 建立 `count` 這個變數,預設值設為 256
  // 並取得修改變數的 `setCount` 方法
  const [count, setCount] = useState(256);

  return (
    <div className="container">
      {console.log('render', count)}
      <div
        className="chevron chevron-up"
        onClick={() => {
          // STEP 3: 使用 setCount 方法來改變 count 的值
          setCount(count + 1);
        }}
      />
      <div className="number">{count}</div>
      <div
        className="chevron chevron-down"
        onClick={() => {
          // STEP 4: 使用 setCount 方法來改變 count 的值
          setCount(count - 1);
        }}
      />
    </div>
  );
};

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

比較一下修改前後的程式碼變更:

Imgur

沒有問題的話,這時候程式應該可以正常運作,點擊箭頭時,數字可以正確更新:

Imgur

同時打開瀏覽器的 console 視窗時,你會發現一旦透過 setCount 方法來改變 count 的數字時,JSX 中的 console.log('render') 就會再被呼叫一次,表示畫面是有重新進行轉譯的:

Imgur

簡單的說,透過 useState 我們建立了一個需要被監控的資料變數(count),而且透過它提供的 setCount 來改變 count 的數值時,React 會幫我們重新轉譯畫面,如此便解決了最上面提到的畫面不會重新轉譯的問題

完整的程式碼範例可以參考這個 CodePen 連結 - Day 6 - Counter with useState

下面我們會再針對剛剛程式碼中各個步驟進行更詳細的說明。

取出 useState 方法來使用

useState 這個方法是放在 React 物件裡面的一個方法,所以要使用它的時候,可以使用 React.useState,或者可以透過物件的解構賦值(object destructuring assignment)來取出 useState 這個方法:

/* useState 是在 React 物件中的一個方法,取用它的方法主要有兩種 */

React.useState(); // 直接透過 `.` 來取用 React 物件內的方法
const { useState } = React; // 透過物件的解構賦值把 useState 方法取出

多數開發者以及 React 官方文件多是使用解構賦值的寫法,因此在後面的不同範例中也都會使用解構賦值的做法來載入 React Hooks。

提示:CodePen 範例中之所以能夠直接取用 React 物件,是因為我們有在 JavaScript Settings 中有先透過 CDN 載入 React 套件。

useState 的使用

為了要在我們的 JavaScript 中可以使用 useState 這個方法,需要在 JavaScript 部份的最上面先把 useState 給取用出來像是這樣:

const { useState } = React;

// ...

取出 useState 這個方法後,一旦我們呼叫了 useState 這個方法,它實際上會回傳一個陣列,這個陣列中的第一個元素會是我們「想要監控的資料」,第二個元素會是「修改該資料的方法」,像是這樣:

// useState() 呼叫後會回傳一個陣列
const arrayReturnFromUseState = useState(<資料預設值>);

// 陣列中的第一個元素是「想要監控的資料」
const count = arrayReturnFromUseState[0];

// 陣列中的第二個元素是「修改該資料的方法」
const setCount = arrayReturnFromUseState[1];

可是每次都要用 [0][1] 這樣的寫法實在是太麻煩了,所以大家也都會直接使用陣列的解構賦值(array destructuring assignment),像這樣:

const [count, setCount] = useState(<資料預設值>);
  • count 是透過 useState() 產生的變數,這是我們希望監控的變數
  • setCount 則是 useState() 產生用來修改 count 這個資料的方法
  • useState() 這個方法的參數中可以帶入資料的預設值

透過 useState 得到的變數和方法名稱是可以自己取的,而慣例上用來改變變數的方法名稱會以 set開頭;預設值也可以不一定要是字串或數值,而是可以帶入物件

下面這些例子都是合法的:

const [price, setPrice] = useState(1000);
const [description, setDescription] = useState('This is description');
const [product, setProduct] = useState({
  name: 'iPhone 11',
  price: 24900,
  os: 'iOS',
});

React 畫面的重新轉譯

上面我們有提到「透過 useState 建立了一個需要被監控的資料變數(count),並且透過 setCount 方法來改變 count 的數值時,React 會幫我們重新轉譯畫面」,這句話需要很仔細的來看。實際上 React 畫面之所以會更新並不是因為 count 的值改變了,而是因為:

  1. setCount 被呼叫到
  2. count 的值確實有改變

這兩個條件缺一不可。釐清這點相當重要,才不會覺得為什麼明明有呼叫 setCount 但畫面沒變,或 count 的值有變但畫面卻沒重新轉譯。

畫面不會更新的情況

下面的程式碼這些都是沒有同時滿足上面這兩個條件,因此畫面不會更新的情況:

沒有使用 setCount 改變變數

這其實是一種錯誤的寫法,既然已經用來 useState 來產生 count 這個變數,表示這個 count 應該是你認為要被 React 監控的資料,但這時候你卻沒有使用 setCount 來改變它,而是直接去改變 count 的值:

Imgur

使用了 setCount 但是 count 的值沒有改變

這裡這樣寫只是為了示範當 count 的值沒變時,畫面並不會重新轉譯,但這不一定是錯誤的寫法。使用了 setCountcount 的值沒有變化時,React 會很聰明的不去做無意義的重新轉譯,因為資料根本就沒變,所以畫面也不需要更新:

Imgur

React 只會更新畫面中有變化的部分

最後,React 在更新畫面時,同樣會很聰明的只去更新有改變的部分,也就是說,它並不會把整個 DOM 都換掉,而是只換掉有變化的部分,也因此才能讓網頁運作的效能大大提升。

從下圖中可以看到,當我們透過按鈕在改變計數器的數字時,React 只會更新 DOM 中有變化的部分:

Imgur

今天我們學習了第一個 React Hooks - useState,明天我們會談談 React Hooks 中的幾個重要原則,並且再根據這個計數器做更多延伸的應用。

程式碼範例

參考資源


上一篇
[Day 05 - 計數器] 將計數器頁面改成用 JSX 來寫
下一篇
[Day 07 - 計數器] 幫計數器設個最大最小值吧 - JSX 中條件渲染的使用
系列文
從 Hooks 開始,讓你的網頁 React 起來30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
janshawn
iT邦新手 5 級 ‧ 2021-06-29 01:10:25

hi 我是正在入門React的新手 看到目前都能夠理解,文章寫得很棒!對我這新手有很大的幫助!
不過這篇文章之中有想要請大大解答~
關於這大標「React 只會更新畫面中有變化的部分」,因為實作部分我也有跟著實作,
那我去看了先前原生JS的寫法,我看DOM的處理也是只針對有變化的再做處理,
這邊就有點不太清楚說"只會更新畫面中有變化的部分"這一塊,
有空的話再麻煩大大解答了!謝謝!

pjchender iT邦新手 3 級 ‧ 2021-07-03 22:14:57 檢舉
janshawn iT邦新手 5 級 ‧ 2021-07-04 20:47:41 檢舉

感謝!

0
ShawnGood
iT邦新手 5 級 ‧ 2022-03-17 13:49:42

感謝分享,原來這就是hook呀
我也分享一下我peek的結果
這個useState看起來每呼叫一次,內部有個變數,就會指向下一塊存資料的地方呢

// STEP 2:
    // 透過 useState 建立 `count` 這個變數,預設值設為 256
    // 並取得修改變數的 `setCount` 方法
    console.log("hook begin")
    const [count, setCount] = useState(256);
    const [x, setX] = useState(256);
    console.log("hook end", count)

webpack打包後的結果
https://ithelp.ithome.com.tw/upload/images/20220317/201060169WEYPmjgUJ.png

我要留言

立即登入留言