iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Modern Web

React 走出新手村 系列 第 11

React 走出新手村-深入 Context Provider

  • 分享至 

  • xImage
  •  

講古

在早期還沒有Context Provider的時候你每一層都需要靠 props 來傳遞資料,才能達到資料共享。

然後...

https://ithelp.ithome.com.tw/upload/images/20230908/20129020mJBa9Tn8QM.png

於是 Context / Provider 就被創造出來了,一個能夠達成跨過不同輩份,也能達到傳值的工具。

這個東西用到了一個 Compound Pattern 的概念,在之後的章節會提到如何使用,用英文字面上的理解也很簡單:

我的內文內容必須要先被創立才能使用它

比如我先創立 MyContext 的結構內容,那我才能成為提供內容的對象,提供內容的對象在英文就是 Provider,所以你要用 Provider 之前必須成為內容的提供者,使用上來說會是 MyContext.Provider,這樣符合邏輯和語意的概念就是 Compound Pattern 的精神。

經歷

其實這個東西從 class component 就存在了,當你從 React 的 Class Component 轉換到 Functional Component 時,會使用 useContext Hooks 來拿取對應的 context 內容。

下面我們將講古一個從 Class Component 到 Functional Component 的轉換範例,同時透過一個使用 useContext 的 Hooks,以達到區域性狀態管理。

假設我們有一個簡單的 counter,我們將示範如何將它從 Class Component 轉換為 Functional Component 並使用 useContext

Class Component 版本:

import React, { Component } from 'react';
// 一樣都需要createContext
const CountContext = React.createContext();

class CounterProvider extends Component {
	// 早期的寫法
  state = {
    count: 0,
  };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
		// 對也是一樣的用法,這裡算是原汁原味
    return (
      <CountContext.Provider value={{ count: this.state.count, increment: this.increment }}>
        {this.props.children}
      </CountContext.Provider>
    );
  };
};

class Counter extends Component {
	// 這裡大概就是唯一的差別了,當你使用static contextType屬性時
	// React會自動將最接近的Context.Provider的值分配給this.context
	// 但也是這裡最容易讓人搞混
  static contextType = CountContext;

  render() {
    const { count, increment } = this.context;
    return (
      <div>
        <p>Count: {count}</p>
        <button onClick={increment}>Increment</button>
      </div>
    );
  };
};

class App extends Component {
  render() {
    return (
      <CounterProvider>
        <div>
          <h1>Counter App</h1>
          <Counter />
        </div>
      </CounterProvider>
    );
  };
};

export default App;

Functional Component 版本:

import React, { useContext, useState } from 'react';

const CountContext = React.createContext();

function CounterProvider({ children }) {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <CountContext.Provider value={{ count, increment }}>
      {children}
    </CountContext.Provider>
  );
}

function Counter() {
  // 採用useCountext並帶入CountContext去取到對應的context
  const { count, increment } = useContext(CountContext);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <div>
        <h1>Counter App</h1>
        <Counter />
      </div>
    </CounterProvider>
  );
}

export default App;

如果還需要圖解的話,我強烈推薦這篇

思考優化

那麼要走出新手村,加上前面也介紹了不少減少重複渲染的方法,我們可以學以致用,來探討怎麼優化我們的Context / Provider。

溫馨提示

一般能夠修改的情況我都推薦使用 Redux toolkit,來做到很完善的 global state management,它不僅是將雜亂的 state 做了整合,更重要的是它早就完成了效能上的優化,如果只是想找方法的話也可以考慮 jotai, zustand… ,這類的工具,已經完成優化了,因為接下來的步驟做完你會發現與這類現成的工具有 87% 像。 → 想學 Redux toolkit 請點此

想了解更多

但也許你是一個 Javascript 的老手,想要自己來,那麼我們就來探討一下這些 state management 工具,是如何達成效能優化的。

首先,我們來看一下範例:

import { createContext, useContext, useState } from "react";

const StoreContext = createContext(null);

const TextInput = ({ value }) => {
  const [store, setStore] = useContext(StoreContext);
  return (
    <fieldset className="field">
      <legend>{value}: </legend>
      <input
        value={store[value]}
        onChange={(e) => setStore({ ...store, [value]: e.target.value })}
      />
    </fieldset>
  );
};

const OrderBtns = ({ value }) => {
  const [store, setStore] = useContext(StoreContext);
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "space-around",
        alignItems: "center"
      }}
    >
      <button
        onClick={() => setStore({ ...store, [value]: (store[value] -= 1) })}
      >
        -
      </button>
      <button
        onClick={() => setStore({ ...store, [value]: (store[value] += 1) })}
      >
        +
      </button>
    </div>
  );
};

const InputContainer = () => {
  return (
    <div className="container">
      <h5>輸入面板</h5>
      <OrderBtns value="count" />
      <TextInput value="name" />
    </div>
  );
};

const Display = ({ value }) => {
  const [store] = useContext(StoreContext);
  return (
    <div className="value">
      {value}: {store[value]}
    </div>
  );
};

const DisplayContainer = () => {
  return (
    <div className="container">
      <h5>已成立訂單</h5>
      <Display value="count" />
      <Display value="name" />
    </div>
  );
};

const ShopContainer = () => {
  return (
    <div className="container">
      <h5>訂單系統</h5>
      <InputContainer />
      <DisplayContainer />
    </div>
  );
};

const ContextExapmle = () => {
  const store = useState({
    count: 0,
    name: ""
  });

  return (
    <StoreContext.Provider value={store}>
      <h1>coffee shop</h1>
      <ShopContainer />
    </StoreContext.Provider>
  );
};

export default ContextExapmle;

上面是從我去年鐵人賽 Redux 教學的範例拆下來的作法,為一個獨立運作的組件,直接在 app 下引用會看到以下畫面:
https://ithelp.ithome.com.tw/upload/images/20230908/20129020J0PnDFfJm3.png

功能應該都會正常,如果按加減按鈕的話會影響 countinput 則直接影響 name,那麼我們使用 chrome 的 react devtool 來查看渲染情況(沒有安裝devtool但想學的看這篇),會發現不論做哪個動作的按鈕,都會造成全部的組件重新渲染,如下:

  • 按下加號
    https://ithelp.ithome.com.tw/upload/images/20230908/20129020E09f2wfJEO.png
  • input 框輸入字
    https://ithelp.ithome.com.tw/upload/images/20230908/20129020QpBdPIH9tu.png

改善方法

你可能會想說簡單,大不了用 memo 包一包就可以解決了啊!

確實是能解決但不完美,尤其是每個外包層的組件都需要額外用memo處理確實是挺麻煩的,而且最外層的 Provider 還是會因為每次的 state change 造成部分組件重新渲染,memo 解決範例如下:

import { createContext, memo, useContext, useState } from "react";

const StoreContext = createContext(null);

const TextInput = ({ value }) => {
  const [store, setStore] = useContext(StoreContext);
  return (
    <fieldset className="field">
      <legend>{value}: </legend>
      <input
        value={store[value]}
        onChange={(e) => setStore({ ...store, [value]: e.target.value })}
      />
    </fieldset>
  );
};

const OrderBtns = ({ value }) => {
  const [store, setStore] = useContext(StoreContext);
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "space-around",
        alignItems: "center"
      }}
    >
      <button
        onClick={() => setStore({ ...store, [value]: (store[value] -= 1) })}
      >
        -
      </button>
      <button
        onClick={() => setStore({ ...store, [value]: (store[value] += 1) })}
      >
        +
      </button>
    </div>
  );
};
// 加入memo讓他不會被觸發不必要的重新渲染
const InputContainer = memo(() => {
  return (
    <div className="container">
      <h5>輸入面板</h5>
      <OrderBtns value="count" />
      <TextInput value="name" />
    </div>
  );
});

const Display = ({ value }) => {
  const [store] = useContext(StoreContext);
  return (
    <div className="value">
      {value}: {store[value]}
    </div>
  );
};

const DisplayContainer = memo(() => {
  return (
    <div className="container">
      <h5>已成立訂單</h5>
      <Display value="count" />
      <Display value="name" />
    </div>
  );
});

const ShopContainer = memo(() => {
  return (
    <div className="container">
      <h5>訂單系統</h5>
      <InputContainer />
      <DisplayContainer />
    </div>
  );
});

const ContextExapmle = () => {
  const store = useState({
    count: 0,
    name: ""
  });

  return (
    <StoreContext.Provider value={store}>
      <h1>coffee shop</h1>
      <ShopContainer />
    </StoreContext.Provider>
  );
};

export default ContextExapmle;

總整問題與思辨

所以,問題還是出在 state change 本身,要想不要觸發的話變成每層都要用memo 的方式處理,或是採用之前提過的 useMemo 或是 useCallback 來處理,也就是說不想這麼麻煩的話就必須採用其他的方法來處理。

這邊留給大家思考的時間,看有沒有辦法想到解決的辦法,下一篇,我再提供我的作法。

給全新手的大禮包

React基本Hook教學


上一篇
React 走出新手村-useMemo & useCallback 小技巧
下一篇
React 走出新手村-自製高效 Context Provider
系列文
React 走出新手村 31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言