iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 12
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 12

【Day.12】React入門 - useState與解構賦值後的props

還記得在Day.04我們實作的觀察者模式嗎? 由於這種「當某個JS變數改變時,有很多DOM元素需要被改變」的功能很常被使用,React將其拉了出來做成獨立的API,稱為state。

使用state會發生什麼事呢?

你可以把state想像成是一種特別的變數。當state被改變時,React會去檢查這個變數在Virtual DOM中所有與其牽連到的地方,並根據state改變後的結果去重新渲染DOM

另外,當props被改變時,React也會做一樣的事情。也就是在React元件中,我們是分別以外部的props和內部的state來控制元件的渲染。

function component的state製造機 - useState

過去,由於ES6的class有提供繼承功能,我們可以藉由繼承React寫好的class來使用state。但是function component就不行。

在React hook推出後,我們可以藉由引入React hook 函式庫來使用state。React hook會把React的相關特性拉到一個集中的邏輯處理器,並依照呼叫的順序執行

useState就是在function component中使用state的React hook。

useState用法

我們可以從React函式庫中取得useState。

import React, {useState} from 'react';

useState是一個函式,他接收一個參數,這個參數會是state的初始值。

useState(false);

還記得我們在Day.04實作的程式碼中,為了要讓變數被改變後可以讓程式去改變其他對應的元素,而限定只能用setIsOpen()來改變isOpen?

useState也是一樣的。

useState這個函式的回傳值是一個Array。Array的第一個值是state變數,第二個值就是「用來改變state」的函式。

console.log(useState(false));

但是難道我們之後在React元件中使用時,都要用arr[0]arr[1]這種方式來呼叫嗎?

這也太不直覺了吧。

這個時候就能運用Javascript中的「解構賦值」語法。

解構賦值

簡單來說,本來當我們要把一個array的東西個別指定給其他變數時,必須要這樣:

const arr = [1,2];
const one = arr[0];
const two = arr[1];

而Javascript提供了這種寫法,讓我們可以一次做完「宣告變數」和「取得陣列中的值」。例如,以下的語法就是宣告onetwo,並將其初始值分別指定給arr[0]arr[1]

const arr = [1,2];
const [one,two] = arr;

console.log(one); //印出 1
console.log(two); //印出 2

另外物件也支援這樣的語法:

const obj = { one: 1, two:2 };
const { one, two } = obj;

console.log(one); //印出 1
console.log(two); //印出 2

把state引入前面的程式碼中吧!

接下來的程式碼都在 src/component/Menu.js中實作。

1. 引入useState

// 這裡其實也是解構賦值歐
import React, {useState} from 'react';

2. 在Menu中接收useState的回傳值

請注意一般會使用set變數名稱來做為第二個回傳值的函數名稱(用來設定state的函式)。

function Menu(props){
    const [isOpen,setIsOpen] = useState(false);
    return (
        <div style={menuContainerStyle}>

3. 把setIsOpen綁在button上,讓isOpen按完變成!isOpen

把函式綁在jsx元素的方式有兩種:

  • { ()=>{函式定義} }
  • { 函式名稱 }不是{ 函式名稱() }

第一種實際上就是在創造一個新的函式,缺點是每次渲染時都會重新創造這個函式,優點是可讀性相對高(因為不會需要去找綁的函式到底是啥、在哪)。

新手建議先以第一種為主。因為第二種方式如果在function component中直接定義函式、使用,不但有可能會使程式碼混雜各種函式、JSX,也會有效能問題,我們會在後面講到useCallback的時候再來討論。

function Menu(props){
    const [isOpen,setIsOpen] = useState(false);
    return (
        <div style={menuContainerStyle}>
            <p style={menuTitleStyle}>{props.title}</p>
            <button style={menuBtnStyle} onClick={()=>{setIsOpen(!isOpen)}}>
                V
            </button>
            <ul>{props.children}</ul>
        </div>
    );
}

4. 用JSX讓其他元素根據isOpen變成對應的樣子

  • 按鈕要對應isOpen顯示「^」「V」
  • 要對應isOpen決定要不要顯示<ul>
function Menu(props){
    const [isOpen,setIsOpen] = useState(false);
    return (
        <div style={menuContainerStyle}>
            <p style={menuTitleStyle}>{props.title}</p>
            <button style={menuBtnStyle} onClick={()=>{setIsOpen(!isOpen)}}>
                {(isOpen)?"^":"V"}
            </button>
            { isOpen && <ul>{props.children}</ul>}
        </div>
    );
}

這樣就完全把前五天的程式碼移植過來了

所有的程式碼:

  • src/component/Menu.js
import React, {useState} from 'react';

const menuContainerStyle = {
    position: "relative",
    width: "300px",
    padding: "14px",
    fontFamily: "Microsoft JhengHei",
    paddingBottom: "7px",
    backgroundColor: "white",
    border: "1px solid #E5E5E5",
};

const menuTitleStyle = {
    marginBottom: "7px",
    fontWeight: "bold",
    color: "#00a0e9",
    cursor: "pointer",
};

const menuBtnStyle = {
    position: "absolute",
    right: "7px",
    top: "33px",
    backgroundColor: "transparent",
    border: "none",
    color: "#00a0e9",
    outline: "none"
}

function Menu(props){
    const [isOpen,setIsOpen] = useState(false);
    return (
        <div style={menuContainerStyle}>
            <p style={menuTitleStyle}>{props.title}</p>
            <button style={menuBtnStyle} onClick={()=>{setIsOpen(!isOpen)}}>
                {(isOpen)?"^":"V"}
            </button>
            { isOpen && <ul>{props.children}</ul>}
        </div>
    );
}

export default Menu;

React hook的其他使用規定

  • 只能在function component和custom hook中使用。

    意思是你不能在class component或是一般JS檔用。至於custom hook我們後面會提到。

  • 只能在function component的最外層scope呼叫useXXX( 也就是不能在迴圈、if-else、在function scope中宣告的function被定義使用。)

第二點是因為當每次元件重新渲染時,React都會呼叫function component的整個定義函式。而前面提過,React hook是用順序來記憶你使用的React hook對應的是在邏輯處理中心的哪個地方。如果今天你寫了:

const [isOpen,setIsOpen] = useState(true);
if(isOpen)
    const [data,setData] = useState(123);
const [str,setStr] = useState("");

假設某次isOpen變成false,因為順序2的const [data,setData] = useState(123)沒有被呼叫到,在那一次渲染時const [str,setStr] = useState("")在元件中的順序會從3變成2,React就會以為const [str,setStr]要接收的是const [data,setData]要接收到的值。導致程式錯誤。

解構賦值與props

還記得我們之前說props是一個物件嗎?

解構賦值除了可以用在取得useState的回傳值,目前也被提倡對props使用。其好處除了可以讓你的同事知道到底傳了什麼東西進props外,未來當你導入Typescript到React中時,也能更快速的知道要怎麼定義interface。

剛剛的程式碼用解構賦值來接收props就會變這樣:

function Menu({ title, children }){
    const [isOpen,setIsOpen] = useState(false);
    return (
        <div style={menuContainerStyle}>
            <p style={menuTitleStyle}>{title}</p>
            <button style={menuBtnStyle} onClick={()=>{setIsOpen(!isOpen)}}>
                {(isOpen)?"^":"V"}
            </button>
            { isOpen && <ul>{children}</ul>}
        </div>
    );
}

setState是非同步的

如果你試了一下,會發現我們在呼叫setState函式時,state並不會馬上被改變。但是有的時候我們就是希望在呼叫setState後做一些事情,這個時候要怎麼處理呢?

我們會在下一篇來講解如何解決這個問題。


上一篇
【Day.11】React入門 - function component、props、children
下一篇
【Day.13】React入門 - useEffect(生命週期)
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言