写点什么

React 高级特性之 Context

  • 2022-12-07
    浙江
  • 本文字数:7627 字

    阅读完需:约 25 分钟

Context 提供了一种不需要手动地通过 props 来层层传递的方式来传递数据。

正文

在典型的 React 应用中,数据是通过 props,自上而下地传递给子组件的。但是对于被大量组件使用的固定类型的数据(比如说,本地的语言环境,UI 主题等)来说,这么做就显得十分的累赘和笨拙。Context 提供了一种在组件之间(上下层级关系的组件)共享这种类型数据的方式。这种方式不需要你手动地,显式地通过 props 将数据层层传递下去。

什么时候用 Context?

这一小节,讲的是 context 适用的业务场景。


Context 是为那些可以认定为【整颗组件树范围内可以共用的数据】而设计的。比如说,当前已认证的用户数据,UI 主题数据,当前用户的偏好语言设置数据等。举个例子,下面的代码中,为了装饰 Button component 我们手动地将一个叫“theme”的 prop 层层传递下去。 传递路径是:App -> Toolbar -> ThemedButton -> Button


class App extends React.Component {  render() {    return <Toolbar theme="dark" />;  }}
function Toolbar(props) { // The Toolbar component must take an extra "theme" prop // and pass it to the ThemedButton. This can become painful // if every single button in the app needs to know the theme // because it would have to be passed through all components. return ( <div> <ThemedButton theme={props.theme} /> </div> );}
class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; }}
复制代码


使用 context,我们可以跳过层层传递所经过的中间组件。现在我们的传递路径是这样的:App -> Button


// Context lets us pass a value deep into the component tree// without explicitly threading it through every component.// Create a context for the current theme (with "light" as the default).const ThemeContext = React.createContext('light');
class App extends React.Component { render() { // Use a Provider to pass the current theme to the tree below. // Any component can read it, no matter how deep it is. // In this example, we're passing "dark" as the current value. return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); }}
// A component in the middle doesn't have to// pass the theme down explicitly anymore.function Toolbar(props) { return ( <div> <ThemedButton /> </div> );}
class ThemedButton extends React.Component { // Assign a contextType to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is "dark". static contextType = ThemeContext; render() { return <Button theme={this.context} />; }}
复制代码

在你用 Context 之前

这一小节,讲的是我们要慎用 context。在用 context 之前,我们得考虑一下当前的业务场景有没有第二种技术方案可用。只有在确实想不出来了,才去使用 context。


Context 主要用于这种业务场景:大量处在组件树不同层级的组件需要共享某些数据。实际开发中,我们对 context 要常怀敬畏之心,谨慎使用。因为它犹如潘多拉的盒子,一旦打开了,就造成很多难以控制的现象(在这里特指,context 一旦滥用了,就会造成很多组件难以复用)。


如果你只是单纯想免去数据层层传递时对中间层组件的影响,那么组件组合是一个相比 context 更加简单的技术方案。


举个例子来说,假如我们有一个叫Page的组件,它需要将useravatarSize这两个 prop 传递到下面好几层的Link组件和Avatar组件:


<Page user={user} avatarSize={avatarSize} />// ... which renders ...<PageLayout user={user} avatarSize={avatarSize} />// ... which renders ...<NavigationBar user={user} avatarSize={avatarSize} />// ... which renders ...<Link href={user.permalink}>  <Avatar user={user} size={avatarSize} /></Link>
复制代码


我们大费周章地将useravatarSize这两个 prop 传递下去,最终只有Avatar组件才真正地用到它。这种做法显得有点低效和多余的。假如,到后面Avatar组件需要从顶层组件再获取一些格外的数据的话,你还得手动地,逐层地将这些数据用 prop 的形式来传递下去。实话说,这真的很烦人。


不考虑使用 context 的前提下,另外一种可以解决这种问题的技术方案是:Avatar组件作为 prop 传递下去。这样一来,其他中间层的组件就不要知道user这个 prop 的存在了。


function Page(props) {  const user = props.user;  const userLink = (    <Link href={user.permalink}>      <Avatar user={user} size={props.avatarSize} />    </Link>  );  return <PageLayout userLink={userLink} />;}
// Now, we have:<Page user={user} />// ... which renders ...<PageLayout userLink={...} />// ... which renders ...<NavigationBar userLink={...} />// ... which renders ...{props.userLink}
复制代码


通过这个改动,只有最顶层的组件Page需要知道Link组件和Avatar组件需要用到“user”和“avatarSize”这两个数据集。


在很多场景下,这种通过减少需要传递 prop 的个数的“控制反转”模式让你的代码更干净,并赋予了最顶层组件更多的控制权限。然而,它并不适用于每一个业务场景。因为这种方案会增加高层级组件的复杂性,并以此为代价来使得低层家的组件来变得更加灵活。而这种灵活性往往是过度的。


在“组件组合”这种技术方案中,也没有说限定你一个组件只能有一个子组件,你可以让父组件拥有多个的子组件。或者甚至给每个单独的子组件设置一个单独的“插槽(slots)”,正如这里所介绍的那样。


function Page(props) {  const user = props.user;  const content = <Feed user={user} />;  const topBar = (    <NavigationBar>      <Link href={user.permalink}>        <Avatar user={user} size={props.avatarSize} />      </Link>    </NavigationBar>  );  return (    <PageLayout      topBar={topBar}      content={content}    />  );}
复制代码


这种模式对于大部分需要将子组件从它的父组件中分离开来的场景是足够有用的了。如果子组件在渲染之前需要与父组件通讯的话,你可以进一步考虑使用 render props 技术。


然而,有时候你需要在不同的组件,不同的层级中去访问同一份数据,这种情况下,还是用 context 比较好。Context 负责集中分发你的数据,在数据改变的同时,能将新数据同步给它下面层级的组件。第一小节给出的范例中,使用 context 比使用本小节所说的“组件组合”方案更加的简单。适用 context 的场景还包括“本地偏好设置数据”共享,“UI 主题数据”共享和“缓存数据”共享等。

相关 API

React.createContext

const MyContext = React.createContext(defaultValue);
复制代码


该 API 是用于创建一个 context object(在这里是指 Mycontext)。当 React 渲染一个订阅了这个 context object 的组件的时候,将会从离这个组件最近的那个Provider组件读取当前的 context 值。


创建 context object 时传入的默认值只有组件在上层级组件树中没有找到对应的的 Provider 组件的时候时才会使用。这对于脱离 Provider 组件去单独测试组件功能是很有帮助的。注意:如果你给 Provider 组件 value 属性提供一个 undefined 值,这并不会引用 React 使用 defaultValue 作为当前的 value 值。也就是说,undefined 仍然是一个有效的 context value。

Context.Provider

<MyContext.Provider value={/* some value */}>
复制代码


每一个 context object 都有其对应的 Provider 组件。这个 Provider 组件使得 Consumer 组件能够订阅并追踪 context 数据。


它接受一个叫 value 的属性。这个 value 属性的值将会传递给 Provider 组件所有的子孙层级的 Consumer 组件。这些 Consumer 组件会在 Provider 组件的 value 值发生变化的时候得到重新渲染。从 Provider 组件到其子孙 Consumer 组件的这种数据传播不会受到 shouldComponentUpdate(这个 shouldComponentUpdate 应该是指 Cousumer 组件的 shouldComponentUpdate)这个生命周期方法的影响。所以,只要父 Provider 组件发生了更新,那么作为子孙组件的 Consumer 组件也会随着更新。


判定 Provider 组件的 value 值是否已经发生了变化是通过使用类似于 Object.is 算法来对比新旧值实现的。


注意:当你给在 Provider 组件的 value 属性传递一个 object 的时候,用于判定 value 是否已经发生改变的法则会导致一些问题,见注意点。

Class.contextType

译者注:官方文档给出的关于这个 API 的例子我并没有跑通。不知道是我理解错误还是官方的文档有误,读者谁知道 this.context 在 new context API 中是如何使用的,麻烦在评论区指教一下。


class MyClass extends React.Component {  componentDidMount() {    let value = this.context;    /* perform a side-effect at mount using the value of MyContext */  }  componentDidUpdate() {    let value = this.context;    /* ... */  }  componentWillUnmount() {    let value = this.context;    /* ... */  }  render() {    let value = this.context;    /* render something based on the value of MyContext */  }}MyClass.contextType = MyContext;
复制代码


组件(类)的 contextType 静态属性可以赋值为一个 context object。这使得这个组件类可以通过 this.context 来消费离它最近的 context value。this.context 在组件的各种生命周期方法都是可访问的。参考 React面试题详细解答


注意:


  1. 使用这个 API,你只可以订阅一个 context object。如果你需要读取多个 context object,那么你可以查看Consuming Multiple Contexts

  2. 如果你想使用 ES7 的实验性特征 public class fields syntax,你可以使用 static 关键字来初始化你的 contextType 属性:


class MyClass extends React.Component {  static contextType = MyContext;  render() {    let value = this.context;    /* render something based on the value */  }}
复制代码

Context.Consumer

<MyContext.Consumer>  {value => /* render something based on the context value */}</MyContext.Consumer>
复制代码


Consumer 组件是负责订阅 context,并跟踪它的变化的组件。有了它,你就可以在一个 function component 里面对 context 发起订阅。


如上代码所示,Consumer 组件的子组件要求是一个 function(注意,这里不是 function component)。这个 function 会接收一个 context value,返回一个 React node。这个 context value 等同于离这个 Consumer 组件最近的 Provider 组件的 value 属性值。假如 Consumer 组件在上面层级没有这个 context 所对应的 Provider 组件,则 function 接收到的 context value 就是创建 context object 时所用的 defaultValue。


注意:这里所说的“function as a child”就是我们所说的 render props 模式。

示例

1. 动态 context

我在这个例子里面涉及到 this.context 的组件的某个生命周期方法里面打印 console.log(this.context),控制台打印出来是空对象。从界面来看,DOM 元素 button 也没有 background。


这是一个关于动态设置 UI 主题类型的 context 的更加复杂的例子:


theme-context.js


export const themes = {  light: {    foreground: '#000000',    background: '#eeeeee',  },  dark: {    foreground: '#ffffff',    background: '#222222',  },};
export const ThemeContext = React.createContext( themes.dark // default value);
复制代码


themed-button.js


import {ThemeContext} from './theme-context';
class ThemedButton extends React.Component { render() { let props = this.props; let theme = this.context; return ( <button {...props} style={{backgroundColor: theme.background}} /> ); }}ThemedButton.contextType = ThemeContext;
export default ThemedButton;
复制代码


app.js


import {ThemeContext, themes} from './theme-context';import ThemedButton from './themed-button';
// An intermediate component that uses the ThemedButtonfunction Toolbar(props) { return ( <ThemedButton onClick={props.changeTheme}> Change Theme </ThemedButton> );}
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() { // The ThemedButton button inside the ThemeProvider // uses the theme from state while the one outside uses // the default dark theme // 以上注释所说的结果,我并没有看到。 return ( <Page> <ThemeContext.Provider value={this.state.theme}> <Toolbar changeTheme={this.toggleTheme} /> </ThemeContext.Provider> <Section> <ThemedButton /> </Section> </Page> ); }}
ReactDOM.render(<App />, document.root);
复制代码

2. 在内嵌的组件中更新 context

组件树的底层组件在很多时候是需要更新 Provider 组件的 context value 的。面对这种业务场景,你可以在创建 context object 的时候传入一个 function 类型的 key-value,然后伴随着 context 把它传递到 Consumer 组件当中:


theme-context.js


// Make sure the shape of the default value passed to// createContext matches the shape that the consumers expect!export const ThemeContext = React.createContext({  theme: themes.dark,  toggleTheme: () => {},});
复制代码


theme-toggler-button.js


import {ThemeContext} from './theme-context';
function ThemeTogglerButton() { // The Theme Toggler Button receives not only the theme // but also a toggleTheme function from the context return ( <ThemeContext.Consumer> {({theme, toggleTheme}) => ( <button onClick={toggleTheme} style={{backgroundColor: theme.background}}> Toggle Theme </button> )} </ThemeContext.Consumer> );}
export default ThemeTogglerButton;
复制代码


app.js


import {ThemeContext, themes} from './theme-context';import ThemeTogglerButton from './theme-toggler-button';
class App extends React.Component { constructor(props) { super(props);
this.toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.dark ? themes.light : themes.dark, })); };
// State also contains the updater function so it will // be passed down into the context provider this.state = { theme: themes.light, toggleTheme: this.toggleTheme, }; }
render() { // The entire state is passed to the provider return ( <ThemeContext.Provider value={this.state}> <Content /> </ThemeContext.Provider> ); }}
function Content() { return ( <div> <ThemeTogglerButton /> </div> );}
ReactDOM.render(<App />, document.root);
复制代码

3. 同时消费多个 context

为了使得 context 所导致的重新渲染的速度更快,React 要求我们对 context 的消费要在单独的 Consumer 组件中去进行。


// Theme context, default to light themeconst ThemeContext = React.createContext('light');
// Signed-in user contextconst UserContext = React.createContext({ name: 'Guest',});
class App extends React.Component { render() { const {signedInUser, theme} = this.props;
// App component that provides initial context values // 两个context的Provider组件嵌套 return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> <Layout /> </UserContext.Provider> </ThemeContext.Provider> ); }}
function Layout() { return ( <div> <Sidebar /> <Content /> </div> );}
// A component may consume multiple contextsfunction Content() { return ( // 两个context的Consumer组件嵌套 <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <ProfilePage user={user} theme={theme} /> )} </UserContext.Consumer> )} </ThemeContext.Consumer> );}
复制代码


但是假如两个或以上的 context 经常被一同消费,这个时候你得考虑合并它们,使之成为一个 context,并创建一个接受多个 context 作为参数的 render props component。

注意点

因为 context 是使用引用相等(reference identity)来判断是否需要 re-redner 的,所以当你给 Provider 组件的 value 属性提供一个字面量 javascript 对象值时,这就会导致一些性能问题-consumer 组件发生不必要的渲染。举个例子,下面的示例代码中,所有的 consumer 组件将会在 Provider 组件重新渲染的时候跟着一起 re-render。这是因为每一次 value 的值都是一个新对象。


class App extends React.Component {  render() {    return (     // {something: 'something'} === {something: 'something'}的值是false      <Provider value={{something: 'something'}}>        <Toolbar />      </Provider>    );  }}
复制代码


为了避免这个问题,我们可以把这种引用类型的值提升到父组件的 state 中去:


class App extends React.Component {  constructor(props) {    super(props);    this.state = {      value: {something: 'something'},    };  }
render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); }}
复制代码

遗留的 API

React 在先前的版本中引入了一个实验性质的 context API。相比当前介绍的这个 context API,我们称它为老的 context API。这个老的 API 将会被支持到 React 16.x 版本结束前。但是你的 app 最好将它升级为上文中所介绍的新 context API。这个遗留的 API 将会在未来的某个大版本中去除掉。


用户头像

还未添加个人签名 2022-09-05 加入

还未添加个人简介

评论

发布
暂无评论
React高级特性之Context_React_夏天的味道123_InfoQ写作社区