React-Context用法汇总 +注意点

Context 用法汇总 + 注意点

前言

父子组件间传递数据,我们可以通过props。但是跨多个组件传递数据时,我们需要一层层往下传递props,代码冗余的同时也降低了可读性。
Context正可以解决这个问题,它可以跨组件提供数据、触发更新,不需要层层下发。
但同时,由于Context的值传递不是链式的,对于Context值更新的触发源的追踪会比较困难。整体的维护成本也会增加。

1.context创建以及取数.jsx

涉及到的Context如下:

  1. React.CreateContext 创建一个Context对象
  2. Context.Provider 提供context数据,函数式、类式组件通用
  3. Context.Consumer 消费context数据,函数式、类式组件通用
  4. 函数式组件中,useContext 消费context数据
  5. 类式组件中,static contextType声明 + this.context 消费context数据

创建Context + 取数Demo代码(可直接运行查看)

jsvascript 复制代码
import React, { Component, useContext } from 'react'

/**
 * 一、context创建以及取数
 * 涉及API:
 * 1、React.createContext()
 * 2. Context.Provider 提供原始数据
 * 3. Context.Consumer 消费原始数据
 * 4. static contextType = DemoContext; 类组件中取数
 * 5. useContext 函数式组件中取数
 */
const DemoContext = React.createContext({
  name: 'zhangsan',
  age: 0,
});

export default class ContextClassDemo extends Component {
  render() {
    return (
      <div>
        {/* 通过Provider提供原始数据 */}
        <DemoContext.Provider value={{ name: 'lisi', age: 18 }}>
          {/* 类式组件访问context */}
          <ClassChildComponent />
          {/* 函数式组件访问context */}
          <FuncChildComponent />
        </DemoContext.Provider>
      </div>
    )
  }
}

// 类组件访问context
class ClassChildComponent extends Component {

  // 1、使用this.context、constructor中访问都需要声明,不然context是空
  // 2、使用Consumer不需要声明
  static contextType = DemoContext;

  constructor(props, context) {
    super(props, context);

    console.log(context, this.context);
  }

  render() {
    const { name, age } = this.context;

    return (
      <div>
        <div>类式组件方案</div>
        <div>用法一 this.context访问</div>
        <div>需要在类中声明 static contextType = DemoContext;</div>
        <div>
          <p>Name: {name}</p>
          <p>Age: {age}</p>
        </div>

        <div>用法二 Consumer组件访问</div>
        <DemoContext.Consumer>
          {value => (
            <div>
              <p>Name: {value.name}</p>
              <p>Age: {value.age}</p>
            </div>
          )}
        </DemoContext.Consumer>
      </div>
    )
  }
}

// 函数组件访问context
function FuncChildComponent() {
  const context = useContext(DemoContext);

  return (
    <div>
      <div>函数式组件方案</div>
      <div>用法一 useContext访问</div>
      <div>
        <p>Name: {context.name}</p>
        <p>Age: {context.age}</p>
      </div>

      <div>用法二 Consumer组件访问</div>
      {/* Consumer方法访问函数式组件、类组件中都适用 */}
      <DemoContext.Consumer>
        {value => (
          <div>
            <p>Name: {value.name}</p>
            <p>Age: {value.age}</p>
          </div>
        )}
      </DemoContext.Consumer>
    </div>
  )
}

2.数据改变-父组件类式组件.jsx

注意点:
DemoContext.Provider判断是否有数据变化是浅比较,只改变传入数据的属性是不会生效的。

例如:

jsvascript 复制代码
this.data = {name: '张三'}
...(省略)
<DemoContext.Provider value={this.data}>
...(省略)
// 每次改变data的属性时,不会触发刷新
this.data.name = 'lisi';

只有当data变化时才会触发:

jsvascript 复制代码
this.data = {name: '张三'}
...(省略)
<DemoContext.Provider value={this.data}>
...(省略)
// data此时指向一个新的对象
this.data = {name: 'lisi'};

数据改变-父组件类式组件

jsvascript 复制代码
import React, { Component, useContext } from 'react'

/**
 * 二、数据改变-父组件类式组件,数据变更
 */
const DemoContext = React.createContext({
  name: 'zhangsan',
  age: 0,
});

export default class ContextClassDataChangeDemo extends Component {
  constructor(props) {
    super(props);

    this.state = {
      name: 'zhangsan',
      age: 0,
    }
  }

  // 年龄+1
  handleAgeAdd = () => {
    this.setState({
      age: this.state.age + 1,
    })
  }

  // 姓名变更
  handleNameChange = () => {
    this.setState({
      name: this.state.name == 'lisi' ? 'zhangsan' : 'lisi',
    })
  }

  render() {
    return (
      <div>
        <div>数据改变</div>
        <div onClick={this.handleAgeAdd}>年龄+1</div>
        <div onClick={this.handleNameChange}>姓名变更</div>

        {/* 通过Provider提供原始数据 */}
        <DemoContext.Provider value={this.state}>
          {/* 类式组件访问context */}
          <ClassChildComponent />
          {/* 函数式组件访问context */}
          <FuncChildComponent />
        </DemoContext.Provider>
      </div>
    )
  }
}

// 类组件访问context
class ClassChildComponent extends Component {

  // 1、使用this.context、constructor中访问都需要声明,不然context是空
  // 2、使用Consumer不需要声明
  static contextType = DemoContext;

  constructor(props, context) {
    super(props, context);

    console.log(context, this.context);
  }

  render() {
    const { name, age } = this.context;

    return (
      <div>
        <div>类式组件方案</div>
        <div>用法一 this.context访问</div>
        <div>需要在类中声明 static contextType = DemoContext;</div>
        <div>
          <p>Name: {name}</p>
          <p>Age: {age}</p>
        </div>

        <div>用法二 Consumer组件访问</div>
        <DemoContext.Consumer>
          {value => (
            <div>
              <p>Name: {value.name}</p>
              <p>Age: {value.age}</p>
            </div>
          )}
        </DemoContext.Consumer>
      </div>
    )
  }
}

// 函数组件访问context
function FuncChildComponent() {
  const context = useContext(DemoContext);

  return (
    <div>
      <div>函数式组件方案</div>
      <div>用法一 useContext访问</div>
      <div>
        <p>Name: {context.name}</p>
        <p>Age: {context.age}</p>
      </div>

      <div>用法二 Consumer组件访问</div>
      {/* Consumer方法访问函数式组件、类组件中都适用 */}
      <DemoContext.Consumer>
        {value => (
          <div>
            <p>Name: {value.name}</p>
            <p>Age: {value.age}</p>
          </div>
        )}
      </DemoContext.Consumer>
    </div>
  )
}

3.数据改变-父组件函数式组件.jsx

数据改变-父组件函数式组件

jsvascript 复制代码
import React, { Component, useContext } from 'react'

/**
 * React Context 数据更新演示
 * 核心逻辑:
 * 1. 父类组件维护Context的数据源(state)
 * 2. 父组件通过setState更新state,Provider的value随之变化
 * 3. 所有消费该Context的子组件会自动重新渲染,获取最新数据
 */

// 1. 创建Context对象,设置默认值(无Provider时生效)
const DemoContext = React.createContext({
  name: '默认名称-zhangsan',
  age: 0,
});

// 根组件:维护Context数据源,并提供更新方法
export default class ContextClassDataChangeDemo extends Component {
  constructor(props) {
    super(props);
    // 2. 父组件state作为Context的数据源
    this.state = {
      name: 'zhangsan',
      age: 0,
    }
  }

  // 年龄+1的更新方法(箭头函数避免this指向问题)
  handleAgeAdd = () => {
    // 注意:更新依赖旧值时,推荐用函数式setState(避免闭包陷阱)
    this.setState(prevState => ({
      age: prevState.age + 1,
    }));
    console.log('年龄已更新:', this.state.age + 1);
  }

  // 姓名切换(zhangsan ↔ lisi)
  handleNameChange = () => {
    this.setState(prevState => ({
      name: prevState.name === 'lisi' ? 'zhangsan' : 'lisi',
    }));
    console.log('姓名已切换');
  }

  render() {
    return (
      <div style={{ padding: '20px', border: '1px solid #eee' }}>
        <h3>Context 数据更新演示(类组件数据源)</h3>
        {/* 交互按钮(增加cursor指针,提升体验) */}
        <div 
          onClick={this.handleAgeAdd}
          style={{ margin: '10px 0', padding: '8px', background: '#e6f7ff', cursor: 'pointer' }}
        >
          点击年龄+1(当前:{this.state.age})
        </div>
        <div 
          onClick={this.handleNameChange}
          style={{ margin: '10px 0', padding: '8px', background: '#f6ffed', cursor: 'pointer' }}
        >
          点击切换姓名(当前:{this.state.name})
        </div>

        {/* 3. Provider的value绑定父组件state,state更新则value更新 */}
        <DemoContext.Provider value={this.state}>
          <hr />
          {/* 类组件消费更新后的Context */}
          <ClassChildComponent />
          <hr />
          {/* 函数组件消费更新后的Context */}
          <FuncChildComponent />
        </DemoContext.Provider>
      </div>
    )
  }
}

// --------------- 类组件消费更新后的Context ---------------
class ClassChildComponent extends Component {
  // 绑定Context,通过this.context访问最新数据
  static contextType = DemoContext;

  // 可选:监听Context更新(生命周期钩子)
  componentDidUpdate(prevProps, prevState) {
    // 对比前后context数据,确认更新
    if (this.context.age !== prevState.age || this.context.name !== prevState.name) {
      console.log('类组件Context数据已更新:', this.context);
    }
  }

  render() {
    const { name, age } = this.context;

    return (
      <div style={{ margin: '10px 0', padding: '10px', background: '#f5f5f5' }}>
        <h4>类组件 - 实时展示Context数据</h4>
        
        <div>✅ 方式1:this.context访问(自动更新)</div>
        <div>姓名:{name} | 年龄:{age}</div>

        <div style={{ marginTop: '10px' }}>✅ 方式2:Consumer访问(自动更新)</div>
        <DemoContext.Consumer>
          {(value) => (
            <div>姓名:{value.name} | 年龄:{value.age}</div>
          )}
        </DemoContext.Consumer>
      </div>
    )
  }
}

// --------------- 函数组件消费更新后的Context ---------------
function FuncChildComponent() {
  // useContext会自动监听Context变化,数据更新时组件重新渲染
  const context = useContext(DemoContext);

  return (
    <div style={{ margin: '10px 0', padding: '10px', background: '#f0f8ff' }}>
      <h4>函数组件 - 实时展示Context数据</h4>
      
      <div>✅ 方式1:useContext访问(自动更新)</div>
      <div>姓名:{context.name} | 年龄:{context.age}</div>

      <div style={{ marginTop: '10px' }}>✅ 方式2:Consumer访问(自动更新)</div>
      <DemoContext.Consumer>
        {(value) => (
          <div>姓名:{value.name} | 年龄:{value.age}</div>
        )}
      </DemoContext.Consumer>
    </div>
  )
}

4.context频繁刷新问题.jsx

涉及API:

  • 1、React.PureComponent
  • 2、shouldComponentUpdate

Context使用时需要注意:

  1. DemoContext.Provider 通过value提供的对象发生变化时,会触发依赖该context的所有组件的刷新
  2. Context触发的更新会跳过 shouldComponentUpdate 判断

因此,当value是通过 <DemoContext.Provider value={``{a, b, c}}> 这种方式提供时,需要额外注意,因为每次该组件刷新时,都会重新构建一个包含a、b、c的新对象,会触发依赖DemoContext的其他组件的刷新。

正常来说,PureComponent 当props没有变化时,不会触发刷新。

但是我们如果通过staticType绑定context,当demo中的身高变化时,类子组件的props没有任何变化,但是还是在控制台打印了'render'。

即使不是PureComponent,强制设置shouldComponentUpdate返回false,也会更新。

解决方案

第一步,可以通过useMemo将传入的value包裹起来,确保只有当需要的属性变化时,触发useMemo的更新,进而触发value变化时,触发子组件刷新。

第二部,拆分Context,避免Context过于臃肿,业务间耦合严重,尽量做到解耦
context频繁刷新问题 Demo

javascript 复制代码
import React, { Component, PureComponent, useContext, useState } from 'react'

/**
 * 四.数据改变-问题-子组件频繁刷新
 * 1、DemoContext.Provider 通过value提供的对象发生变化时,会触发用到该context的组件的刷新
 * 2、Context触发的更新会跳过 shouldComponentUpdate 判断
 * 综上,Provider value的变化会带来不必要的刷新
 * 
 * 涉及API:
 * 1、React.PureComponent
 * 2、shouldComponentUpdate
 */
const DemoContext = React.createContext({
  name: 'zhangsan',
  age: 0,
});

export default function ContextClassDataChangeDemo() {
  const [data, setData] = useState({
    name: 'zhangsan',
    age: 0,
  })
  
  const [height, setHeight] = useState(170)

  const addAge = () => {
    setData({
      ...data,
      age: data.age + 1,
    })
  }

  const changeName = () => {
    setData({
      ...data,
      name: data.name == 'lisi' ? 'zhangsan' : 'lisi',
    })
  }

  const incrementHeight = () => {
    setHeight(height + 1)
  }

  return <div>
    <div>
      <button onClick={addAge}>年龄+1</button>
      <button onClick={changeName}>姓名变更</button>
      <button onClick={incrementHeight}>身高+1</button>
    </div>

    <DemoContext.Provider value={{name: data.name, age: data.age}}>
      <ClassChildComponent />
      <FuncChildComponent height={height}/>
    </DemoContext.Provider>
  </div>
}

// 类组件访问context
class ClassChildComponent extends PureComponent {

  // 1、使用this.context、constructor中访问都需要声明,不然context是空
  // 2、使用Consumer不需要声明
  static contextType = DemoContext;

  constructor(props, context) {
    super(props, context);

    console.log(context, this.context);
  }

//   shouldComponentUpdate(nextProps, nextContext) {
//     // 返回false,可以观测到
//     // context数据变化时,视图更新了
//     // props.height变更的时候,视图无变化没变
//     // 结论:
//     //   context会忽略shouldComponentUpdate的结果,所以对于Context.Provider传入的value需要慎重对待,
//     //   频繁更新Contet会带来不必要的视图刷新
//     console.log('shouldComponentUpdate');
//     return false
//   }

  render() {
    const { name, age } = this.context;

    console.log('类-子组件 render触发',name, age);
    return (
      <div>
        <div>类式组件方案</div>
        <div>用法一 this.context访问</div>
        <div>需要在类中声明 static contextType = DemoContext;</div>
        <div>
          <p>Name: {name}</p>
          <p>Age: {age}</p>
        </div>

        <div>用法二 Consumer组件访问</div>
        <DemoContext.Consumer>
          {value => (
            <div>
              <p>Name: {value.name}</p>
              <p>Age: {value.age}</p>
            </div>
          )}
        </DemoContext.Consumer>
      </div>
    )
  }
}

// 函数组件访问context
function FuncChildComponent(props) {
  const context = useContext(DemoContext);

  return (
    <div>
      <div>函数式组件方案</div>
      <div>用法一 useContext访问</div>
      <div>
        <p>Name: {context.name}</p>
        <p>Age: {context.age}</p>
      </div>

      <div>用法二 Consumer组件访问</div>
      {/* Consumer方法访问函数式组件、类组件中都适用 */}
      <DemoContext.Consumer>
        {value => (
          <div>
            <p>Name: {value.name}</p>
            <p>Age: {value.age}</p>
          </div>
        )}
      </DemoContext.Consumer>

      <div>Height:{props.height}</div>
    </div>
  )
}

// TODO

// React 源码阅读中,预计Context也是基于发布订阅模式实现

相关推荐
Cao_Ron7 分钟前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy060112 分钟前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku13 分钟前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者15 分钟前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron26 分钟前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee34 分钟前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup41 分钟前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬1 小时前
短链跳转
前端
孟陬1 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai
Cao_Ron1 小时前
给 ECharts 6 graph 节点叠加角标:从原理到性能优化
前端