React 使用Context传递参数

全文共 2310 个字

Context

在使用React时,很容易在自定义的React组件之间跟踪数据流。当监控一个组件时,可以监控到那些props被传递进入组件了,这非常有利于了解数据流在什么地方出现了问题。

在某些情况下,开发者想要通过组件树直接传递数据,而不是在一层又一层的组件之间手工传递数据。此时,可以使用React的“context”特性接口来快速实现这个功能。

尽量不要使用Context

React在16.x版本之后算是将Context调整为正式接口,不过还是建议如果组件之间传递数据的层次不算太深,尽量不要使用Context。而且 Redux MobX 等技术能提供比Context特性更为优雅的实现方式。

最新实现方式

Context功能在16.x之后所有的API和使用方法都发生了巨大的改变,如果你使用的是最新版本(16.x)看这里最新方式就够了,如果是较早的版本,请看下方的历史实现小节。

新版本的Context实现方式简洁清晰许多,方式还是以类似于 高阶组件 包裹的方式为主。

入门使用案例

这是一个没有使用Context特性3个组件组合的使用例子:

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

function Toolbar(props) {
  //为了让子组件能获取必要的参数,这里需要使用props.theme继续向子组件传递参数,
  //但是实际上theme参数对于Toolbal组件来说并没有任何价值。
  //例如项目全局设置了一个theme参数来控制很多组件的主题样式,
  //那么这个参数需要在几乎所有的组件出现,并且不断的传递他
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

function ThemedButton(props) {
  return <Button theme={props.theme} />;
}

上面的theme参数表示全局主题样式,很多组件通过他来控制自己当前应该呈现的样式。如果我们在根组件控制这个参数,那么几乎所有的组件都要向下传递这个参数。

下面是用Context特性实现的方式:

// 创建一个Context组件,可以理解为一种特殊的高阶组件。
// 'light'是当前的默认值
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    //使用Provider将子组件包裹起来。
    //将值修改为'dark'
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

//中间组件,并不关心和他无关的参数
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

//使用参数的组件
function ThemedButton(props) {
  // 使用Consumer组件包裹需要获取参数的组件
  // 在这个案例中,命名为light的Context被赋值"dark",然后在Consumer中获取这个值
  return (
    <ThemeContext.Consumer>
      {theme => <Button {...props} theme={theme} />}
    </ThemeContext.Consumer>
  );
}

上面就是简单使用Context的例子,16.x之后也是通过高阶组件的方式来实现,是不是看了之后感觉很想Redux。只要是通过 Provider 包裹的组件,在其后的整个组件树中都可以用 Consumer 来获取指定的数据。

上面的代码我们也可以修改为下面这样更直观的形式:

const {Provider, Consumer} = React.createContext('light');
class App extends React.Component {
  render() {
    return (
      <Provider value="dark">
        <Toolbar />
      </Provider>
    );
  }
}
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
function ThemedButton(props) {
  return (
    <Consumer>
      {theme => <Button {...props} theme={theme} />}
    </Consumer>
  );
}

使用Context需要注意:

  1. 由于 Provider 和 Consumer都是返回一个组件,所以我们最好设定一个默认的context.value,以防止出现渲染错误。
  2. 当Provider发生数据变更时,会触发到 Consumer 发生渲染,所有被其包裹的子组件都会发生渲染(render方法被调用)。

任意组件更新Context

某些时候需要在内部组件需要去更新Context的数据,其实我们仅仅需要向上下文增加一个回调即可,看下面的例子:

//创建Context组件
const ThemeContext = React.createContext({
  theme: 'dark',
  toggle: () => {}, //向上下文设定一个回调方法
});

function Button() {
  return (
    <ThemeContext.Consumer>
      {({theme, toggle}) => (
        <button
          onClick={toggle} //调用回调
          style={{backgroundColor: theme}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

//中间组件
function Content() {
  return (
    <div>
      <Button />
    </div>
  );
}

//运行APP
class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggle = () => { //设定toggle方法,会作为context参数传递
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    this.state = {
      theme: themes.light,
      toggle: this.toggle,
    };
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state}> //state包含了toggle方法
        <Content />
      </ThemeContext.Provider>
    );
  }
}

App组件创建了Provider,并向其参数传递了一个回调方法,之后任何使用了 Consumer 的子孙组件都可以使用这个回调方法了触发更新。

多个Context复合使用

React支持设置多个Context,看下面的例子:

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

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>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

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

和使用单个Context也没多大区别,相互包装一层即可。

使用Context时需要牢记一点:和Redux一样,只要 Provider 的value发生变更都会触发所有 Consumer 包裹的子组件渲染。

16.x之后的Context使用起来比旧版本的简单明了太多,实现思路上还是学习了Redux等将状态抽取出来统一管理并触发更新的方式来实现,在使用时选择一种方式来实现就行。

历史实现

如何使用Context

假设有下面这样一个组件结构:

class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.props.color}}>
        {this.props.children}
      </button>
    );
  }
}

class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text} <Button color={this.props.color}>Delete</Button>
      </div>
    );
  }
}

class MessageList extends React.Component {
  render() {
    const color = "purple";
    const children = this.props.messages.map((message) =>
      <Message text={message.text} color={color} />
    );
    return <div>{children}</div>;
  }
}

在上面的例子中,在最外层组件手工传入一个color属性参数来指定Button组件的颜色。如果使用Context特性,我们可以直接将属性自动的传递给整个组件树:

const PropTypes = require('prop-types');

class Button extends React.Component {
  render() {
    // 注意this.context.color
    return (
      <button style={{background: this.context.color}}>
        {this.props.children}
      </button>
    );
  }
}

// 限定color属性只接收string类型的参数
Button.contextTypes = {
  color: PropTypes.string
};

class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text} <Button>Delete</Button>
      </div>
    );
  }
}

class MessageList extends React.Component {
  // 在后续组件中设定一个Context的值
  getChildContext() {
    return {color: "purple"};
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

//限定子组件的color值只接收string类型的参数
MessageList.childContextTypes = {
  color: PropTypes.string
};

通过在 MessageList 组件(Context的制定者)中增加  childContextTypes 和 getChildContext ,React会自动将这个指定的context值传递到所有子组件中(比如例子中的 Button组件),而子组件也可以定义一个 contextTypes 来指定接收context的内容。如果未定义子组件的 contextTypes ,那么调用  context 只能得到一个空对象。

父子组件耦合

Context特性还可以让开发人员快速构建父组件与子组件之间的联系。例如在 React Router V4 包中:

import { BrowserRouter as Router, Route, Link } from 'react-router-dom';

const BasicExample = () => (
  <Router>
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/topics">Topics</Link></li>
      </ul>

      <hr />

      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route path="/topics" component={Topics} />
    </div>
  </Router>
);

例子通过Router组件传递一些数据,每一个被Router包含的 Link 和 Route 都可以直接通信。但是建议在使用这些API构建组件时,先思考是否还有其他更清晰的实现方式。例如可以使用回调的方式去组合组件。

在生命周期方法中引入Context

如果在某个组件上定义了 contextTypes ,下面这些生命周期方法将会接收到额外的参数——  context 对象。我们这里这样调整参数接口:

在无状态的方法性组件中引入Context

无状态的方法性组件也可以引入Context,前提是给组件定义了 contextTypes 。下面的代码展示了在无状态的组件—— Button 中引入context的表达式:

const PropTypes = require('prop-types');

const Button = ({children}, context) =>
  <button style={{background: context.color}}>
    {children}
  </button>;

Button.contextTypes = {color: PropTypes.string};

更新Context

首先,千万不要更新Context。

React提供一个更新Context的接口,但是它会从根本上破坏React的结构所以建议不要使用他。

getChildContext 在state或props变更时会被调用。为了更新context中的数据可以使用 this.setState方法来触发变更,触发之后context的更新会被子组件接收到。

const PropTypes = require('prop-types');

class MediaQuery extends React.Component {
  constructor(props) {
    super(props);
    this.state = {type:'desktop'};
  }

  getChildContext() {
    return {type: this.state.type};
  }

  componentDidMount() {
    const checkMediaQuery = () => {
      const type = window.matchMedia("(min-width: 1025px)").matches ? 'desktop' : 'mobile';
      if (type !== this.state.type) {
        this.setState({type});
      }
    };

    window.addEventListener('resize', checkMediaQuery);
    checkMediaQuery();
  }

  render() {
    return this.props.children;
  }
}
MediaQuery.childContextTypes = {
  type: PropTypes.string
};

这里的问题在于,如果一个context在组件变更时才产生,接下来如果中间某个组件的 shouldComponentUpdate方法返回fasle值,那么后续组件无法从context中得到任何值。所以,如果使用context来维护管理状态,那么就需要从全局去控制组件,这和React单向数据流和组件化的思路有些背道而驰。而且随着应用的扩展以及人员的更变,全局管理状态会越来越难。如果你还想了解更多关于context的问题,可以阅读这篇博客文章——“How To Safely Use React Context"(翻墙),里面讨论了如果绕开这些问题。