# Context

* Context 提供了一个无需为每层组件手动添加 props，就能在组件树间进行数据传递的方法
* 在一个典型的 React 应用中，数据是通过 props 属性自上而下进行传递的
  * 但这种做法对于某些类型的属性来说是极其繁琐的
  * Context 提供了一种在组件之间共享此类值的方法，而不是显式地通过组件树的逐层传递 props
* [如何使用Context](#如何使用context)
* [使用Context之前的考虑](#使用context之前的考虑)
* [API](#api)
  * [React.createContext](#react.createcontext)
  * [Context.Provider](#context.provider)
  * [Class.contextType](#class.contexttype)
  * [Context.Consumer](#context.consumer)
  * [Context.displayName](#context.displayname)
* [实例](#实例)
  * [动态Context](#动态context)
  * [在嵌套组件中更新Context](#在嵌套组件中更新Context)
  * [使用多个Context](https://github.com/zg-zhang/note/tree/e13141b69420373bbac3b25db40358a37cd56fc1/react/advanced-guides/%E4%BD%BF%E7%94%A8%E5%A4%9A%E4%B8%AAontext/README.md)
* [注意事项](#注意事项)

## 如何使用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

&#x20;// ... 渲染出 ...

&#x20;// ... 渲染出 ...

&#x20;// ... 渲染出 ...

````
* 相比较上面层层传递 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>
    )
}
```

}

\`\`\`
