iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 17
2
Modern Web

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

【Day.17】React入門 - 利用useContext進行多層component溝通

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


我們完成了分頁,完成了用React製作的一個Menu和MenuItem,看似一切都大功告成了。

然而不幸的是,這個時候客戶打來了一通電話:

欸!我想讓MenuItem被點擊的時候,Menu會自動關起來,這應該不難吧!

因為BD覺得不難、PM覺得不難,所以你看著目前的架構,開始說服自己這個不難:

  • src/page/MenuPage.js
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;
  • src/component/MenuItem.js
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;
  • src/component/Menu.js
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的概念就誕生了。

Context API - React 的 Global資料

Global的概念就像是住宅大廈的公共設施,它不單獨屬於任何一個人,也能夠被任何人取用。

React內建提供了一個實作Global資料的方法,稱作Context API。使用方法是使用

React.createContext(Context初始值)

現在,請創建src/context資料夾,並創立一個檔案ControlContext.js,在其中定義OpenContext,其初始值為一個物件,裡面一次包好等等要用的openContextsetOpenContext

import React from "react";

export const OpenContext = React.createContext({
    openContext: true,
    setOpenContext: ()=>{},
})

Provider - 提供Context

那麼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中創造isOpensetIsOpen。並把他丟給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;

useContext - 在function Component中使用Context

在function component中,取用Context的方式是先引入useContext和目標context後,透過

const data = useContext(xxxContext);

data就會是剛剛存好的那個物件。

現在請在Menu中,先把isOpensetIsOpen拿掉,然後用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在業界必用到的東西吧!


上一篇
【Day.16】React入門 - 想要分頁? react-router-dom
下一篇
【Day.18】開發者工具React Dev tool與useContext的效能問題
系列文
從比入門再往前一點開始,一直到深入React.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言