📒REACT笔记👉👉👉Context(上下文)

ReactContext特性有过一些印象,之前觉得就是组件跨级传输数据,现在看完文档总结一下。

Context的使用场景应该为👉 不同层级的组件需要一样的数据,比如:

0️⃣ 首选语言

1️⃣ 全局的主题

2️⃣ 当前认证的用户

比如说我们有一个theme属性,而我们的Button组件则需要根据theme属性来渲染不同的UI效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const App = () => {
  return <MiddleComponent theme='dark'>
}

const MiddleComponent = ({theme}) => {
  return <ThememButton theme={theme}>
}

class ThemeButton extends React.Component {
  render() {
    const backgroundColorMap = {
      dark: 'black',
      light: 'blue',
    }
    return <button style={{backgroundColor: backgroundColorMap[this.props.theme]}}/>
  }
}

从上面的代码我们不难发现,MiddleComponent组件根本不需要theme属性,只不过因为它是目标子组件(ThemeButton)的父级组件,所以我们不得不经过这个组件。现在这样来看其实还好,组件的层级树还不是很复杂,假如ThemeButton藏得更深,那我们岂不是要层层手动传递下去,而途经的这些组件显然根本不需要theme属性!再发散一点,如果有一个ThemeInput也需要theme属性,这样的噩梦还有尽头吗?好的,我们现在使用React.createContext来改写这个例子逃出噩梦。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const ThemeContext = React.createContext('light');

const App = () => {
  return (
    <ThemeContext.Provider value='dark'>
      <MiddleComponent />
    </ThemeContext.Provider>
  )
}

const MiddleComponent = () => {
  return <ThememButton />
}

class ThemeButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    const backgroundColorMap = {
      dark: 'black',
      light: 'blue',
    }
    return <button style={{backgroundColor: backgroundColorMap[this.context.theme]}}/>
  }
}

解释一下我们做了什么,我们调用React.createContext方法生成一个ThemeContext对象,它有两个属性: Provider、Consumer,属性值皆为组件!然后我们在组件的最外层用React.Provider包裹,并且定义value属性,是的就这么简单,只要我们进行了这样的操作,那么React.Provider的任一子组件都可以拿到这个value了。稍等,我们还是需要做点什么的!在我们的ThemeButton组件内,我们用了static关键字声明了一个静态属性,且将ThemeContext赋值给它,这次真的所有操作结束了!我们就可以通过this.context拿到value了!!!!

为了以防有人不知道static用法,解释一下,它跟ThemeButton.contextType = ThemeContext是一样的效果,只不过是一种语法糖罢了。

不知道有人注没注意到我们的ThemeButton是一个类组件,是的,这种static方式只能作用于类组件,函数式组件不允许这样做。别担心,React为我们想到了所有可能的情况,我刚才在上面提到过,生成的Context对象它有两个属性: Provider、Consumer。我们可以使用Consumer组件来承接context:

1
2
3
4
5
6
7
<MyContext.Counsumer>
{
  (value) => {
    return <FunctionCom contextProp={value} />
  }
}
</MyContext.Counsumer>

几乎所有的React开发人员都会安装React DevTool插件在浏览器中调试,所以我们需要给我们Provider、Consumer组件一个名字让他们区分开来,我们可以通过给context对象displayName属性赋值来实现:

1
2
3
4
MyContext = React.createContext('defaultValue')
MyContext.displayName = 'MyConetxt!'

// 在浏览器里组件分别会标记为👉 MyContext!.Provider/ MyContext!.Consumer 

在此之前我们说的context都是静态不变的,有的时候我们的context需要动态被改变,比如放到state里之后被setState改变:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class App extends React.Component {

constructor(props) {
    super(props);
    this.state = {
      theme: 'light',
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === 'light'
            ? 'dark'
            : 'light',
      }));
    };
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        <Toolbar />
      </ThemeContext.Provider>
      <ButtonChangeTheme changeTheme={this.toggleTheme} />
    );
  }
}

const ButtonChangeTheme = ({changeTheme}) => <button onClick={changeTheme} />

class ThemeButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    const backgroundColorMap = {
      dark: 'black',
      light: 'blue',
    }
    return <button style={{backgroundColor: backgroundColorMap[this.context.theme]}}/>
  }
}

然而有的时候更新context函数也需要被一同传递到到子级组件,我们需要一起传下去:

1
2
3
4
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

就剩最后一个点了,那就是我们定义value属性的时候,尽量不要用行内样式传value<Context.Provider value={{a: 10}}></Context.Provider>,这样当Provider的父组件重渲染时,Consumer组件也会被更新,因为相当于生成了一个新的value对象,那么该怎么解决呢?最好把value放到state里:this.state = {value: {a: 10}}

居然还有一个点....就是我们使用React.createContext(defaultValue)中的defauliValue什么时候会生效的问题,可能下意识会认为是当Provider组件的valueundefined或者null时才生效。都不是,而是当Consumer组件没有找到对应的Provider时,才会使用defaultValue,这样其实方便我们独立地测试Consumer组件。

这次真的没了👋

updatedupdated2020-07-062020-07-06