iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Modern Web

React 從 0.5 到 1系列 第 10

[鐵人賽 Day10] Context(下)-花式用法

  • 分享至 

  • xImage
  •  

Context 的各種情境

實際的情境可能更加複雜,如果我們希望切換樣式的按鈕,是底下深層的子元件呢?我可以一次 pass 好幾個 context 下去嗎?React 文件提供了更多情境的範例,給大家參考。

情境一:如何我希望切換狀態的元件,不在父層?

該如何把切換狀態的元件往下傳,從子層做到父層狀態的控制?除了把控制狀態的函式當作 props 一層一層傳遞,這裡也有一個不用層層傳遞的方法:將 toggle method 包在 context 內。

// 建立 context 的時候,先把 toggle method 定義進去
export const ThemeContext = React.createContext({
  theme: themes.night,
  toggleTheme: () => {},
});

// 將切換的功能與 state 在父層設定好,跟著 theme 一起包著傳下去
...
this.state = {
      theme: themes.morning,
      toggleTheme: this.toggleTheme,
};
....
render() {
    return (
			// 這裡傳下去的 value 就包含了樣式與 toggle 樣式的方法
      <ThemeContext.Provider value={this.state}>
        <ThemeTogglerButton />
      </ThemeContext.Provider>
    );
  }

// 在準備放置切換功能的子元件上,取用隨著 value 傳下來的切換的功能
// * ThemeContext.Consumer 需要放一個函式在子層,這個函式會接收現在的 context,詳細 API 解釋在下方

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme !
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

情境二:如何我有很多個 context,會打架嗎?

比較複雜的網站可能會有很種類型的 Global 狀態,為了不要拖累 contect re-rendering 的速度,比較好的做法是讓每個 context 區分開來。

如果有些 context 常常被一起使用,你也可以考慮自己設計一個可以同時提供二者的 render prop component(見之後的文章)。

// 假設我有一組共用樣式、一組共用的使用者狀態,我需要分別為他們建立 context
const ThemeContext = React.createContext('morning');
const UserContext = React.createContext({
  identity: 'Guest',
});

class App extends React.Component {
  render() {
    // 在最上層的父元素,將他們一併往下傳,採用層層包覆的寫法
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

// 這裡是會使用到資料的元件位置
// 分別取到值之後,傳進子元件 <Avatar> 內
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <Avatar user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

情境三:context 的陷阱,以及避開的方法

Context 如何辨識何時該 re-render context 內容?靠的是 reference,所以有時候也會有一些意料之外的情況,當 provider 的父層 re-render 時,意外帶動下層的所有 consumers(訂閱與取用 context 的地方)一起 re-render,因為當父層在 re-render 的時候,會為了 value 不斷重新建立新的物件。

關於辨識 value 值改變的方法,可以參考 Object.is() 以及下方 API 有更完整的說明。

class App extends React.Component {
  render() {
    return (
			// 當 App re-render 的時候,value 的物件也不斷被重新建立
      <MyContext.Provider value={{something: 'something'}}>
        <Layout />
      </MyContext.Provider>
    );
  }
}

如何解決這種問題?把 value 存放進父層的 state 裡,就可以被保留住。

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
			// 把 value 裡的物件存放在這裏,就不會隨著 re-render 被洗掉重建
      value: {something: 'something'},
    };
  }

  render() {
    return (
			// 將 state 直接往下傳
      <MyContext.Provider value={this.state.value}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

Context API - 了解 API 運作方式

  1. React.createContext
const MyContext = React.createContext(defaultValue);

createContext 會建立一個 context 物件,當 React render 訂閱了這個 context 的元件後,會去拿到當下的 context 值(從最接近的上層 Provider 取得)。

createContext 會接收一個參數 defaultValue,只有當元件沒有相對應的上層 Provider 時才會用到。如果在 Provider 內傳入 undefined,並不會導致子元件吃到 defaultValue。

  1. Context.Provider
<MyContext.Provider value={/* some value */}>

每個被建造出來的 context 物件,都會有其相對應的 Provider,讓底下的子元件可以訂閱 context 的改變。

Provider 元件會接受一個 value 值,value 值會被傳遞給底下需要使用的元件,一個 Provider 可以有很多個需求方。同樣 Provider 也有可能是很多層、但離子元件越遠的資訊、會被更靠近的覆蓋。

當 value 改變的時候,在 Provider 底下,並且有使用到 context 的元件都會一起 re-render,但這個行為並不會被 shouldComponentUpdate 方法捕捉到。provider 是如何判斷 value 的改變?他會去比較舊值與新值,使用跟 Object.is() 相同的演算法。

  1. Class.contextType
class MyClass extends React.Component {
  render() {
    let value = this.context;
  }
}
MyClass.contextType = MyContext;

class.contextType 可以被指派給我們建立出來的 context。使用這個 property,你可以在 class component 內使用 this.context 取得現在的 value,跟底下的 Context.Consumer 不同的是,你可以在生命週期方法裡,使用 this.context 的值。

不過使用這個方法,你只能訂閱一個 context。

  1. Context.Consumer
// 例如這個例子

function ThemeTogglerButton() {
 
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

Consumer 也會去訂閱 context 的改變,需要一個函示放在他的底下,會接受現在的 context 並回傳一個 React node。不同之處在於可以讓你在 function component 內訂閱 context。

如果你使用的是 Hook,請使用 useContext

const value = useContext(MyContext);

useContext 會接收 React.createContext 建立出來的 context 物件,並且 return 當下的 context value,相當於上面提到的 contextType & Consumer,不過雖然可以取代這兩個,你還是需要 Provider 把 value 向下傳。


上一篇
[鐵人賽 Day09] React Context(上)-單純的用法
下一篇
[鐵人賽 Day11] React 原始碼的初見面 ——官方 codebase 指南
系列文
React 從 0.5 到 115
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言