[铁人赛 Day10] Context(下)-花式用法

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 向下传。


<<:  Day 13【连动 MetaMask - Back-End Services】这显然是厂商的疏失

>>:  找LeetCode上简单的题目来撑过30天啦(DAY10)

Day 20:会议

前言 除了开发新功能,开会占据了我们许多时间,如果能够节省时间出来,我们才能去学些新技术、重构、甚至...

LeetCode解题 Day12

882. Reachable Nodes In Subdivided Graph https://l...

[Day 28] 储存训练好的模型

储存训练好的模型 今日学习目标 使用 pickle + gzip 储存模型 将训练好的模型打包并储存...

Day02: 02 - 前端 - 开启专案、页面刻划、bootstrap-vue使用

Hi,搭给贺,我是Charlie! 在Day01当中,我们安装了所需的套件跟软件,还做了版面。 接下...

Snail

今日kata 原始题目如下:(4kyu) Given an n x n array, return ...