(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
還記得在Day.04我們實作的觀察者模式嗎? 由於這種「當某個JS變數改變時,有很多DOM元素需要被改變」的功能很常被使用,React將其拉了出來做成獨立的API,稱為state。
你可以把state想像成是一種特別的變數。當state被改變時,React會去檢查這個變數在Virtual DOM中所有與其牽連到的地方,並根據state改變後的結果去重新渲染DOM。
另外,當props被改變時,React也會做一樣的事情。也就是在React元件中,我們是分別以外部的props和內部的state來控制元件的渲染。
過去,由於ES6的class有提供繼承功能,我們可以藉由繼承React寫好的class來使用state。但是function component就不行。
在React hook推出後,我們可以藉由引入React hook 函式庫來使用state。React hook會把React的相關特性拉到一個集中的邏輯處理器,並依照呼叫的順序執行。
useState就是在function component中使用state的React hook。
我們可以從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提供了這種寫法,讓我們可以一次做完「宣告變數」和「取得陣列中的值」。例如,以下的語法就是宣告one
和two
,並將其初始值分別指定給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
接下來的程式碼都在 src/component/Menu.js
中實作。
// 這裡其實也是解構賦值歐
import React, {useState} from 'react';
請注意一般會使用set變數名稱
來做為第二個回傳值的函數名稱(用來設定state的函式)。
function Menu(props){
const [isOpen,setIsOpen] = useState(false);
return (
<div style={menuContainerStyle}>
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>
);
}
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>
);
}
這樣就完全把前五天的程式碼移植過來了
所有的程式碼:
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;
意思是你不能在class component或是一般JS檔用。至於custom hook我們後面會提到。
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是一個物件嗎?
解構賦值除了可以用在取得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
函式時,state
並不會馬上被改變。但是有的時候我們就是希望在呼叫setState
後做一些事情,這個時候要怎麼處理呢?
我們會在下一篇來講解如何解決這個問題。