(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
我們完成了分頁,完成了用React製作的一個Menu和MenuItem,看似一切都大功告成了。
然而不幸的是,這個時候客戶打來了一通電話:
欸!我想讓MenuItem被點擊的時候,Menu會自動關起來,這應該不難吧!
因為BD覺得不難、PM覺得不難,所以你看著目前的架構,開始說服自己這個不難:
import React from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
return (
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
);
}
export default MenuPage;
import React from 'react';
const menuItemStyle = {
marginBottom: "7px",
paddingLeft: "26px",
listStyle: "none"
};
function MenuItem(props){
return <li style={menuItemStyle}>{props.text}</li>;
}
export default MenuItem;
import React 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>
);
}
想了很久,你得出了這個結論:
所以就是把Menu的setIsOpen傳到MenuPage再傳進去MenuItem裡面嘛!
於是你又開始思考要怎麼把Menu的setIsOpen傳到父元件MenuPage中...
難道沒有更好的做法嗎?
這個「把Menu的setIsOpen傳到MenuPage再傳進去MenuItem裡面」的作法雖然的確可行,但是可讀性超差,而且有太多要考量的事情。當專案一大起來,我們更不可能這樣做,不然程式碼就全部都會被沒有用的傳遞函式和props給占滿了。
「有沒有一個全局的state和setState可以讓所有的元件共同操作呢?」
於是Global State的概念就誕生了。
Global的概念就像是住宅大廈的公共設施,它不單獨屬於任何一個人,也能夠被任何人取用。
React內建提供了一個實作Global資料的方法,稱作Context API。使用方法是使用
React.createContext(Context初始值)
現在,請創建src/context
資料夾,並創立一個檔案ControlContext.js
,在其中定義OpenContext,其初始值為一個物件,裡面一次包好等等要用的openContext
和setOpenContext
。
import React from "react";
export const OpenContext = React.createContext({
openContext: true,
setOpenContext: ()=>{},
})
那麼React要如何把Context隔空提供給各個元件使用呢?答案是用一個叫做<Provider>
的元件把所有需要這個Context的元件包起來。Provider會內建於Context中,所以使用方式就是<xxxContext.Provider>
現在,請在src/page/MenuPage
引入OpenContext,並用<OpenContext.Provider>
把所有的東西包起來。
import React from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
return (
<OpenContext.Provider>
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
</OpenContext.Provider>
);
}
export default MenuPage;
但是現在Context裡面的資料只是普通變數而已,並不是State。所以現在我們要在MenuPage中創造isOpen
和setIsOpen
。並把他丟給OpenContext。
綁給OpenContext的方式是利用OpenContext.Provider的一個props - value
:
<OpenContext.Provider value={綁定值}>
</OpenContext.Provider>
實際加入程式碼中:
import React, {useState} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
const [isOpen, setIsOpen] = useState(true);
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
return (
<OpenContext.Provider value={{
openContext: isOpen,
setOpenContext: setIsOpen
}} >
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
</OpenContext.Provider>
);
}
export default MenuPage;
在function component中,取用Context的方式是先引入useContext
和目標context後,透過
const data = useContext(xxxContext);
data就會是剛剛存好的那個物件。
現在請在Menu中,先把isOpen
和setIsOpen
拿掉,然後用useContext
引入OpenContext
。
import React, {useContext, useMemo} from 'react';
import { OpenContext } from '../context/ControlContext';
function Menu(props){
const isOpenUtil = useContext(OpenContext);
這個isOpenUtil就會是剛剛存好的
isOpenUtil = {
openContext: isOpen,
setOpenContext: setIsOpen
}
所以現在,我們先把Menu本來綁定isOpen
的button改成用isOpenUtil
裡面的東西看看能不能運作(先不管ul要不要顯示)
function Menu(props){
const isOpenUtil = useContext(OpenContext);
return (
<div style={menuContainerStyle}>
<p style={menuTitleStyle}>{props.title}</p>
<button style={menuBtnStyle} onClick={
()=>{isOpenUtil.setOpenContext(!isOpenUtil.openContext)}
}>
{(isOpenUtil.openContext)?"^":"V"}
</button>
<ul>{props.children}</ul>
</div>
);
}
恩,看起來能跟本來一樣的方法運作!
由於在<OpenContext.Provider></OpenContext.Provider>
裡面的元件不管隔幾層、在哪裡都能取用OpenContext
,我們就能用這種方式達成多層子父元件的溝通。
現在,你應該有能力可以在MenuItem中實作剛剛前面講的功能了!
請先引入useContext
到MenuItem中,但不要讓它跟return值有關。
import React, {useContext} from 'react';
import { OpenContext } from '../context/ControlContext';
const menuItemStyle = {
marginBottom: "7px",
paddingLeft: "26px",
listStyle: "none"
};
function MenuItem(props){
const isOpenUtil = useContext(OpenContext);
return <li style={menuItemStyle}>{props.text}</li>;
}
export default MenuItem;
這裡我們之所以還不實作讓MenuItem可以控制開關的功能,不是因為中秋節我想偷懶,而是因為useContext本身存在一個效能問題。我們會在接下來的幾篇中來發現跟討論如何解決這些問題。
如果你只是想先熟悉React語法,想試著寫小專案的人,到目前為止的內容已經可以讓你有能力做這件事了。如果有哪些還不清楚,可以搭配我去年的文章,理論上大部份初學者使用React會遇到的問題,我都有把解法寫在這兩個系列中了。
但是如果你是想靠React找工作的人這樣是不夠的。尤其是useContext的這個效能問題是中大型專案必須要面對到的。如果我沒有忙到爆炸的話我們接下來就繼續聊聊更多React在業界必用到的東西吧!