Context

Context

如何使用Context

  • Context 设计目的是为了共享那些对于一个组件树而言是 全局 的数据

    ```jsx harmony

// 不使用 Context class App extends React.Component { render() { return ; } }

function Toolbar(props) { return ( ) }

class ThemedButton extends React.Component { render() { return } }

```jsx harmony

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树
// 为当前的 theme 创建一个 context 默认值为 ‘light’
const ThemeContext = React.createContext('light');

class App extends React.Component {
    render() {
        return (
            // 使用一个 Provider 来将当前的 theme 传递给以下的组件树
            // 无论多深,任何组件都能读取这个值
            // 在这个例子中,我们将 dark 作为当前的值传递下去
            <ThemeContext.provider value='dark'>
                <Toolbar/>
            </ThemeContext.provider>
        )
    }
}

// 中间的组件再也不必指明往下传递 theme 了
function Toolbar() {
    return (
        <div>
            <ThemedButton/>
        </div>
    )
}

class ThemedButton extends React.Component {
    // 指定 contextType 读取当前 theme context
    // React 会往上找到最近的 theme Provider,然后使用它的值
    // 在这个例子中,当前的 theme 值为 dark
    static contextType = ThemeContext;
    render() {
        return <Button theme={this.context}/>
    }
}

使用Context之前的考虑

  • Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据

    • 谨慎使用 因为这会使组件的复用性变差

  • 如果你只是想避免层层传递一些属性,组件组合 component composition 有时是一个比 context 更好的解决方案

```jsx harmony

// ... 渲染出 ...

// ... 渲染出 ...

// ... 渲染出 ...

* 相比较上面层层传递 props 有一种无需 context 的方法 就是 将 Avatar 组件自身传递下去
* 这种变化下,只有最顶部的 page 组件需要知道 Link 和 Avatar 组件是如何使用 user 和 avatarSize 的
* 但是这样的问题是,这并不适用于每一个场景,这种将逻辑提升到组件树的更高层次来处理
    * 会使这些高层组件变的更复杂
    * 并且会强行将低层组件适应这样的形式,这可能不会是你想要的
* 而且你的组件并不限制于接收单个子组件,你可能会传递多个子组件,甚至会为这些子组件 children 封装多个单独的 接口 slots

```jsx harmony

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

Api

React.createContext

```jsx harmony

const MyContext = React.createContext(defaultValue);

* 创建一个 Context 对象
* 当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件数中离自身最近的那个匹配的 Provider 中读取当前的 context 值
* 只有当前组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。
    * 有助于在不使用 Provider 包装组件的情况下对组件进行测试
    * 将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效

### Context.Provider
```jsx harmony

<MyContext.Provider value={/* value */}>
  • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化

  • 一个 Provider 可以和多个消费组件有对应关系 一个 Provider 也可以嵌套使用 里面的会覆盖外层的数据

  • 当 Provider 的值发生变化时,它内部的所有消费组件都会重新渲染

    • Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数

    • 因此当 consumer 组件在其祖先组件退出更新的情况下也能更新

  • 通过新旧值检测来确定变化,使用了 Object.is 相同的算法

Class.contextType

  • 挂载在 class 上的 contextType 属性会被重新赋值为一个由 React.createContext() 创建的 Context 对象

    • 这能让你使用 this.context 来消费最近的 Context 上的那个值

    • 你可以在任何生命周期函数中访问到它,包括 render 函数中

      ```jsx harmony

class MyClass extends React.Component { componentDidMount() { let value = this.context; / 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 / } componentDidUpdate() { let value = this.context; / ... / } componentWillUnmount() { let value = this.context; / ... / } render() { let value = this.context; / 基于 MyContext 组件的值进行渲染 / } } MyClass.contextType = MyContext;

### Context.Consumer
```jsx harmony

<MyContext.Consumer>
    {value => /* 基于 context 值进行渲染 */}
</MyConetxt.Consumer>
  • React 组件也可以订阅到 context 的变更

    • 在函数式组件中完成订阅 context

  • 这个函数接收当前的 context 值,返回一个 React 节点

    • 传递给函数的 value 值等同于往上组件树离这个 context 值最近的 Provider 提供的 value 值

    • 如果没有相应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue

Context.displayName

  • context 对象接受一个名为 displayName 的 property,类型为字符串

  • React DevTools 使用该字符串来确定 context 要显示的内容

示例

动态Context

```jsx harmony

const themes = { light: { foreground: '#000000', background: '#eeeeee' }, dark: { foreground: '#ffffff', background: '#222222' } };

const ThemeContext = React.createContext( themes.light );

class ThemedButton extends React.Component { render() { let props = this.props; let theme = this.context; return ( ) } } ThemedButton.contextType = ThemeContext;

function Toolbar(props) { return ( Change Theme ) }

class App extends React.Component { constructor(props) { super(props); this.state = { theme: themes.light };

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

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

}

### 在嵌套组件中更新Context
* 从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context
```jsx harmony

const ThemeContext = React.createContext({
    theme: themes.dark,
    toggleTheme: () => {}
});

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

function Content() {
    return (
        <div>
            <ThemeTogglerButton />
        </div>
    )
}

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

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

        this.state = {
            theme: themes.light,
            toggleTheme: this.toggleTheme,
        }
    }

    render() {
        return (
            <ThemeContext.Provider value={this.state}>
                <Content/>
            </ThemeContext.Provider>
        )
    }
}

消费多个Context

  • 为了确保 context 快速进行重渲染,React需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点

    ```jsx harmony

const ThemeContext = React.createContext('light'); const UserContext = React.createContext({ name: 'Guest', });

function Content() { return ( {theme => ( {user => ( user: {user} theme: {theme} )} </UserContext.Consumer> )} </ThemeContext.Consumer> ) }

function Layout() { return ( ) }

class App extends React.Component { render() { const {signedInUser, theme} = this.props;

    return (
        <ThemeContext.Provider value={theme}>
            <UserContext.Provider value={signedInUser}>
                <Layout />
            </UserContext.Provider>
        </ThemeContext.Provider>
    )
}

}

* 如果两个或更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值

## 注意事项
* 因为 context 会使用参考标识 reference identity 来决定何时进行渲染,这里可能会有一些陷阱
    * 当 Provider 的父组件进行重渲染时,可能会在 Consumers 组件中触发意外的渲染
    * 当每一次 Provider 重渲染时,以下的代码会重新渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象

```jsx harmony

class App extends React.Component {
    render() {
        return (
            <MyContext.Provider value={{something: 'something'}}>
                <Toolbar />
            </MyContext.Provider>
        )
    }
}
  • 为了避免这种情况,将 value 状态提升到父节点的 state 里

```jsx harmony

class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'something'} } }

render() {
    return (
        <Provider value={this.state.value}>
            <Toolbar />
        </Provider>
    )
}

}

```

最后更新于