摆脱 Prop Drilling 的噩梦:React Context 深入浅出

74 阅读5分钟

在 React 应用开发中,我们经常需要在组件之间传递数据。当组件树结构简单时,使用 props 可以轻松地完成这项任务。但随着应用复杂度的提升,组件嵌套层级越来越深,props 的层层传递(Prop Drilling)就变成了一个令人头疼的问题。它不仅使代码冗余、难以维护,也降低了组件的复用性。

Prop Drilling 之痛

想象一下这样一个场景:你需要在应用的顶层组件获取用户信息,然后将其传递给深层嵌套的子组件。如果没有其他机制,你就不得不一层一层地将用户信息通过 props 传递下去,即使中间层的组件根本不需要使用这些数据。

import React, { Component } from "react";

export default class App extends Component {
  render() {
    const user = { name: "小明", role: "admin" };
    return <Header user={user} />;
  }
}

class Header extends Component {
  render() {
    return (
      <div>
        <Navigation user={this.props.user} />
      </div>
    );
  }
}

class Navigation extends Component {
  render() {
    return (
      <div>
        <UserPanel user={this.props.user} />
      </div>
    );
  }
}

class UserPanel extends Component {
  render() {
    return <div>欢迎 {this.props.user.name}</div>;
  }
}

可以看到这种开发模式存在几个比较明显的问题:

  1. props drilling问题:中间层组件(如Header、Navigation)即使不需要使用user数据,也必须接收并向下传递这些props
  2. 代码冗余:每增加一层组件,都需要手动添加props传递逻辑
  3. 维护困难:当需要修改传递的数据结构时,需要在多个层级中进行同步修改
  4. 组件耦合:组件间因props传递而形成强依赖关系,降低了组件的独立性和可复用性

React Context:共享数据的优雅方案

为了解决 Prop Drilling 的问题,React 提供了 Context API。它允许我们在组件树中创建一个上下文对象,并在任何子组件中直接访问该对象,而无需通过 props 层层传递。

React Context 是什么?

Context 主要由3部分组成:

  1. React.createContext: 创建一个Context对象
  2. Context.Provider: 提供数据的组件
  3. Context.ConsumeruseContext Hook:消费数据的组件

将上述例子使用Context 改造:

import React, { Component } from "react";
const UserContext = React.createContext({
  name: "",
  user: "",
});
export default class App extends Component {
  render() {
    const user = { name: "小明", role: "admin" };
    return (
      <UserContext.Provider value={user}>
        <Header />
      </UserContext.Provider>
    );
  }
}

class Header extends Component {
  render() {
    return (
      <div>
        <Navigation />
      </div>
    );
  }
}

class Navigation extends Component {
  render() {
    return (
      <div>
        <UserPanel />
      </div>
    );
  }
}

class UserPanel extends Component {
  render() {
    return (
      <div>
        欢迎
        <UserContext.Consumer>
          {(value) => {
            return value.name;
          }}
        </UserContext.Consumer>
      </div>
    );
  }
}

可以看到改造后的代码简单了许多,只需要在最上层组件使用Provider 提供数据,在子组件中需要使用到组件使用Consumer 即可获取到数据。

React Context 的好处

解决props drilling问题: 避免了不必要的中间层props传递

代码简洁性: 减少了冗余代码,使组件更加清晰

全局状态管理: 适合应用级别的状态共享(如主题、用户认证信息等)

动态更新: 当Provider的value变化时,所有消费组件都会重新渲染

Context 中displayName的作用

Context 除了createContext,Provider, Consumer 之外,还有一个属性displayName,displayName 作用主要是在调试的时候更方便,在React Dev Tools中看到具体名称,来看下displayName的具体使用方式:

const UserContext = React.createContext({
  name: "",
  user: "",
});
UserContext.displayName="User Context"
//省略后面的代码
...

效果:

image.png 在没使用displayName的时候,名字都是用默认名字Context.Provider

image.png displayName 有什么好处呢?

在只有一个Context 的时候你可能看不出来它有什么好处,但是当你有很多个Context的时候它的好处就体现出来了,有很多个Context的时候,如果没有displayName,想找到对应的Context 进行调试会非常难找,而使用displayName之后就能很快找到对应的Context。

contextType

Context 还有一个重要的特性contextType,它只适用于类组件。来看下具体的使用

import React, { Component } from "react";
const UserContext = React.createContext({
  name: "",
  user: "",
});

// UserContext.displayName = "User Context";
export default class App extends Component {
  render() {
    const user = { name: "小明", role: "admin" };
    return (
      <UserContext.Provider value={user}>
        <Header />
      </UserContext.Provider>
    );
  }
}

class Header extends Component {
  render() {
    return (
      <div>
        <Navigation />
      </div>
    );
  }
}

class Navigation extends Component {
  render() {
    return (
      <div>
        <UserPanel />
      </div>
    );
  }
}

class UserPanel extends Component {
  static contextType = UserContext;
  render() {
    return (
      <div>
        欢迎
        {this.context.name}
      </div>
    );
  }
}

可以看到我们将之前的Context消费组件UserPanel进行了改造,将UserContext.Consumer 进行了删除,在这组件中添加了一个特殊的变量contextType, 并且给它赋值为UserContext,这样React会自动给这个组件的实例上新增一个context,我们在组件中就可以使用this.context 获取到UserContext.Provider 提供的值。

注意: contextType 是React 规定的一个特殊变量,这个变量只能赋值一个Context,否则会在浏览器控制台看见警告。比如我们将static contextType = UserContext; 改成 static contextType = 123;

image.png

过度使用React Context的问题

虽然 Context 解决了 Prop Drilling 的问题,但过度使用也会带来一些负面影响:

难以追踪数据流: 过度使用 Context 会使数据流变得难以追踪,难以理解组件的行为和数据来源。

组件重渲染: Context 的更新会触发所有消费该 Context 的组件重新渲染,即使它们并不依赖于更新的数据。这可能会导致性能问题。

破坏组件封装: Context 使得组件可以访问其祖先组件的状态,这可能会破坏组件的封装性,增加组件之间的耦合度。

总结

React Context是React提供的一种强大工具,有效解决了props层层传递的问题。它简化了组件间的状态共享。然而,如同所有强大的工具一样,Context需要谨慎使用。理解其适用场景和潜在问题,才能在实际开发中做出合理的选择。