2019年7月3日

[React] React Context API 以及 useContext Hook 的使用

imgur
圖片來源:algolia blog

基本概念與使用

透過 React Context API 可以將資料傳到直接傳送到需要的元件,而不需要手動一直透過 props 傳入:
  • Context 是設計來在 React 元件中共享資料,類似在 React 元件的全域(global),這些資料類似「使用者的登入狀態」、「樣式(theme)」、「檢視語言(preferred language)」、「資料快取(cache)」。
  • Context 主要用在當多個不同嵌套層級的元件要用到相同的資料時才會只用,除此之外,應該盡量避免使用,因為它會使得元件更難被復用(reuse)
如果你只是想要避免一直傳遞 props 到每一層, component composition 通常是一個更簡單的方式。

Context 的使用方式

讀取資料Context 的方式是在根元件的地方(例如,App.js)透過 <Context.Provider> 把需要傳遞的資料帶入 Context 中,而這些資料通常會放在根目錄的 state 中;接著,在需要使用到此資料的地方,在透過 contextType 取得 context 內的資料。
修改資料:若有需要修改 Context 中的內容,則是去修改根元件的 state 後重新帶入 <Context.Provider> 中,如此子層的元件就可以再次透過 contextType 取得新的資料,因此把修改 state 的函式放在 state 中是很常見的一種做法
import { MyContext } from './src/contexts'; // 透過 React.createContext 建立

class Foo extends React.Component {
  constructor() {
    this.state = {
      size: '2x',
      changeSize: (size) => this.setState({ size }),
    };
  }

  render() {
    return (
      <MyContext.Provider value={this.state}>
        <App />
      </MyContext.Provider>
    );
  }
}
🔖 所以實際上真正操作資料的地方是在根元件的,而 Context 更像是修改和傳遞資料的一個媒介而已

STEP 0:建立 SwitchThemeButton 元件

// ./src/ThemedButton
import React from 'react';

class ThemedButton extends React.Component {
  render() {
    return <button>Change Theme</button>;
  }
}

export default ThemedButton;

STEP 1:建立 Context

透過 Context API 可以不用透過 props 一直將資料傳到各元件內,透過 React.createContext API 建立一個 context,裡面的內容為預設值
// ./src/AppContext.js

// STEP 1: createContext(<預設值>)
export const AppContext = React.createContext({
  theme: 'light',
  toggleTheme: () => {},
});
:::caution
預設值被使用的時機使當開發者沒有使用 <Context.Provider> 卻又使用了 useContext 去取值時會用到;一旦使用 Context.Provider 後,就會以 <Context.Provider value={} />value 帶入的值為主。
:::

STEP 2:使用 Context.Provider

透過 Context.Provider 中的 value 屬性,可以把想要的值傳入內部的每個元件,:
// ./src/App.js
// ...
import { AppContext } from './AppContext';

class App extends React.Component {
  constructor(props) {
    super(props);

    // 定義修改 state 的方法
    this.toggleTheme = () => {
      this.setState(prevState => ({
        theme: prevState.theme === 'dark' ? 'light' : 'dark',
      }))
    }

    // 在 state 中放入修改 state 的方法,並傳入 <Context.Provider> 中
    this.state = {
      theme: 'dark',
      toggleTheme
    }
  }

  render() {
    // STEP 2: Use Context.Provider
    // 將 state 的資料放入 Provider 中
    return (
      <AppContext.Provider
        value={this.state}
      >
        <Toolbar />
      </AppContext.Provider>
    );
  }
⚠️ 因為 value 內如果放物件的話,每次都算是全新的物件,因此為了避免經常重新轉譯,可能的話將 Context.Provider 屬性 value 內的值放在 state 中,使用 value={this.state} 這種做法。

STEP 3:定義 contextType 以取得 context 內容

若想要在元件的個生命週期中使用 this.context 取得 Context 的值,需要先將該元件定義 contextType,React 會找在與該元件最接近的 Context Provider,並且將可以在 render 時取用到它的值。定義 contextType 的方法有兩種:
// ./src/ThemedButton.js
// ...
import { AppContext } from './src/AppContext';

class ThemedButton extends React.Component {
  // STEP 3: 方法一,透過 static 定義 contextType
  static contextType = AppContext; // 才可以使用 this.context

  render() {
    // 在 render 中將可以使用 this.context
    const { theme, toggleTheme } = this.context;
    return (
      <button theme={theme} onClick={toggleTheme}>
        {theme}
      </button>
    );
  }
}

// STEP 3: 方法二 定義 contextType
ThemedButton.contextType = AppContext; // 才可以使用 this.context

在 Functional Component 中使用 Context 的值

如果 functional component 需要使用 Context 的值,可以透過 Context.Consumer 元件,其內部需要帶入 function,該 function 的參數即可取得 Context 的值
/*
<AppContext.Consumer>
  {(value) => { ... }}
</AppContext.Consumer>
*/
function Toolbar(props) {
  return (
    <div>
      <AppContext.Consumer>
        {({ theme, size }) => (
          <p>
            theme: {theme} <br />
            size: {size}
          </p>
        )}
      </AppContext.Consumer>
      <ThemedButton />
    </div>
  );
}

示範影片與程式碼

React Hooks - useContext

useContext 中可以在 function 中使用 useContext(MyContext),這等同於在 class 中使用 static contextType = MyContext,或者是 <MyContext.Consumer> 的用法。
import { MyContext } from './src/contexts';

const value = useContext(MyContext);
若把上面範例中的 ThemedButton 元件改成 useContext 的寫法,則會變成:
import React, { useContext } from 'react';
import { ThemeContext } from './../contexts/ThemeContext';

const SwitchThemeButton = () => {
  const context = useContext(ThemeContext);
  const { theme, toggleTheme } = context;
  return (
    <button
      style={{
        color: theme.foreground,
        backgroundColor: theme.background,
      }}
      onClick={toggleTheme}
    >
      Change Theme
    </button>
  );
};

export default SwitchThemeButton;

其他

沒有使用 context API 的情況

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

const Toolbar = (props) => {
  // 這裡 Toolbar 元件必須有多一個額外的 "theme" prop,並帶入到 ThemedButton 中
  // 然而,如果每一個在 App 內的按鈕都需要知道 theme 的話,這樣傳遞資料會變得非常麻煩
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
};

class ThemedButton extends React.Component {
  render() {
    return <button>{this.props.theme}</button>;
  }
}

使用 context API 後

要使用 Context Value 的地方要先將該 class 定義 contextType,如此才可以在各個生命週期使用 this.context取得 Context 的內容。定義的方法包括:
const AppContext = React.createContext({
  theme: 'light',
  size: '2x',
});

// 定義 contextType
class ComponentUseContext extends React.Component {
  static contextType = AppContext; // 才可以使用 this.context
}

// 如果在 class 是沒寫 static contextType = AppContext;
// 則需要額外在這定義 contextType
ThemedButton.contextType = AppContext; // 才可以使用 this.context
完整程式內容:
import React from 'react';
import logo from './logo.svg';
import './App.css';

// STEP 1: createContext(<defaultValue>)
// 透過 Context API 可以不用透過 props 一直將資料傳到各元件內
// 建立一個 context,並以 light 為預設值
const AppContext = React.createContext({
  theme: 'light',
  size: '2x',
});

class App extends React.Component {
  render() {
    // STEP 2: Use Context
    // 使用 Provider 可以將當前的 theme 傳入內部的每個子元件
    // 每個元件都能讀取到它,不論它有多深
    return (
      <AppContext.Provider
        value={{
          theme: 'dark',
          size: '1x',
        }}
      >
        <Toolbar />
      </AppContext.Provider>
    );
  }
}

// 中間層的元件不需要做任何事
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // STEP 3: 取得 AppContext 的值
  // 指定 contextType 來讀取當前的 AppContext 的值
  // React 會找在 AppContext 的 Provider 最接近的那個,並使用它的值
  // 這裡會是 "dark"
  static contextType = AppContext; // 才可以使用 this.context

  render() {
    const { theme, size } = this.context;
    return (
      <button theme={theme} size={size}>
        {theme}, {size}
      </button>
    );
  }
}

// 如果在 class 是沒寫 static contextType = AppContext;
// 則需要額外在這定義 contextType
ThemedButton.contextType = AppContext; // 才可以使用 this.context

export default App;

參考

0 意見:

張貼留言