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也是基于发布订阅模式实现

相关推荐
用户0595401744621 分钟前
把AI对话记忆存储测试从手工改成Playwright+pytest,覆盖率从20%提到96%,回归时间缩短90%
前端·css
kyriewen27 分钟前
别再这样写TypeScript了——Code Review中最常见的8个反模式
前端·javascript·typescript
名字还没想好☜34 分钟前
Next.js ‘use client‘ 到底加在哪:Server/Client Components 边界与常见报错
开发语言·前端·javascript·react·next.js
午安~婉1 小时前
GitHub Token/ GitHub Stats统计显示图异常
前端·github·vercel·github token
码云之上1 小时前
AI Agent 工程化总览篇:从 Prompt 到 Harness
前端·人工智能
2501_926978331 小时前
以说明书 DNA 为模板——完整 AGI 的结构图景
前端·人工智能·经验分享·笔记·ai写作
IT_陈寒1 小时前
Vite静态资源引用这个坑我踩得有点疼
前端·人工智能·后端
程序员黑豆2 小时前
鸿蒙应用开发:AttributeModifier 使用教程
前端·harmonyos
codeniu2 小时前
TRAE Work 实战 | 从"有个想法"到线上Demo,我全程只靠对话就完成了
前端·trae
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十):增加管理员角色组管理
前端·后端·ai编程