在前端开发中,提到context,我们最先想到的或许是"执行上下文"。事实上,在前端开发中,context的含义有多个点:
- Execution Context(执行上下文) :在 JavaScript 中,每段代码在执行时都处于一个执行上下文中。这个上下文包含了变量对象、作用域链、this 指向等信息。JavaScript 中有全局执行上下文和函数执行上下文,它们决定了变量和函数的可访问性。
- This 指向 :在 JavaScript 中,函数的执行上下文决定了
this
关键字的指向。this
指向当前执行代码的对象,它的值取决于函数被调用的方式。 - 作用域链:执行上下文中的作用域链决定了函数可以访问哪些变量和函数。
- React Context:在 React 应用中,"context" 是一种特殊的全局状态管理机制。它允许你在组件树中传递数据,而不需要手动逐层传递 props。
- ...
既然是在React.js专栏中,本篇文章要讲的就是React Context
。
React Context有什么用
上文提到:
在 React 应用中,"context" 是一种特殊的全局状态管理机制。它允许你在组件树中传递数据,而不需要手动逐层传递 props。
在 React 开发中,如果不使用状态管理工具和context,数据的传递通常是通过 props 属性进行的,即将数据通过 UI 树显式传递到使用它的组件。
如上图,如果组件2-1想要使用组件1-1的数据,由于是单项数据流,数据需要状态提升 到App组件,然后通过props
一层层传递。
然而,这种方式增加了组件之间的耦合性,降低组件的可复用性。在组件树比较深或者需要在多个层级之间传递相同数据时会显得非常繁琐和冗余。
为了简化跨组件传递数据的过程,React 在16.3 版本引入了 Context API。React Context
是 React 提供的原生机制,用于跨组件传递数据。
Context为整个组件树提供数据,Context 会穿过中间的任何组件。官网图示:
说到React中的状态管理,第一个想到的就是redux
。既然有了redux
,为什么要学context
?
事实上,React Redux 的实现是基于 React Context 的。React Redux 在内部利用了 React 的 Context API 来将 Redux 的 Store 提供给整个应用中的组件。
下面,我们来看context的具体使用。
context的使用
官方文档:使用 Context 深层传递参数 -- React 中文文档
1. 创建context对象------React.createContext
js
// context.js
import React from 'react';
export const MyContext = React.createContext();
React.createContext()创建了一个上下文对象,该对象提供了Provider(提供者)
和Consumer(消费者)
。
另外,在创建context的时候可以传入参数作为默认值,这个我们到后面再说。
2. 提供数据的组件------Context.Provider
jsx
// App.jsx
import SubComponent from "./components/SubComponent";
import { MyContext } from "./context";
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
// 这里的value将后面的数据放入到上下文环境中
<MyContext.Provider value={{ count, setCount, name: '张三' }}>
{/* Provider 内部的组件树 */}
<SubComponent />
</MyContext.Provider>
);
}
export default App;
SubComponent组件中有一些子组件:
js
import React from 'react';
import Child1 from "./Child1";
import Child2 from "./Child2";
import Child3 from "./Child3";
export default function SubComponent() {
return (
<div>
SubComponent
<Child1 />
<Child2 />
<Child3 />
</div>
);
}
接下来我们就要在这些子组件中使用Provider提供的数据。
3. 消费数据------Context.Consumer、useContext
想要使用Context的数据,可以有两种方式。
第一种:useContext (推荐)
React 16.8 之后,引入了 Hooks, useContext
提供了一种更简洁的方式来消费 Context。该 Hook 接收一个由 React.createContext API 创建的上下文对象,并返回该 context 的当前值。
jsx
// Child1.jsx
import { useContext } from 'react';
import { MyContext } from "../context"
export default function ChildCom4() {
const { count, setCount, name } = useContext(MyContext);
return (
<div style={{ border: '1px solid', width: "200px",}}>
Child1
<div>count:{count}</div>
<button onClick={() => setCount(count + 1)}>增加</button>
<div>name:{name}</div>
</div>
);
}
运行,可以看到在child1中可以获取到Provider组件中的value,并且能使用传递过来的方法。
第二种:Context.Consumer
这是在useContext之前的老方法。Consumer 是用来消费数据的组件,即订阅指定 Context 的变化,并在变化发生时重新渲染其内部的子组件。
其基本结构是通过一个函数作为其子元素(children)来订阅 Context 的值。��个函数接收当前 Context 的值作为其唯一的参数,并返回一个 React 元素。例如:
js
<MyContext.Consumer>
{value => /* 根据 value 做一些操作 */}
</MyContext.Consumer>
示例:
js
import React from 'react';
import { MyContext } from "../context";
export default function ChildCom2() {
return (
// Consumer从上下文中读取数据
<MyContext.Consumer>
{(context) => (
<div style={{ border: '1px solid', width: "200px" }} >
Child2
<div>count:{context.count}</div>
<button onClick={() => context.setCount(context.count + 1)}>增加</button>
<div>name:{context.name}</div>
</div>
)}
</MyContext.Consumer>
);
}
运行,可以看到这样的效果和useContext是一样的。
class组件可使用contextType
以上两种方法都可用于函数组件。class组件可以像以上第二个示例一样使用Consumer组件,也可以使用静态属性 contextType
。使用很简单,就是把Context 对象赋值给挂载在 class 上的 contextType 属性。
js
export default class Child2 extends Component {
// 指定 contextType 读取当前的 context对象
static contextType = MyContext;
render() {
return (
<div>
<button onClick={() => this.context.setCount(this.context.count + 1)}>增加</button>
<div>count:{this.context.count}</div>
</div>
)
}
}
进阶使用
1. 使用多个Context 上下文环境
在刚才的context.js文件中再创建一个context:
js
import React from "react";
export const MyContext = React.createContext();
export const MyContext2 = React.createContext();
使用Provider提供数据:
js
import SubComponent from "./components/SubComponent";
import { MyContext, MyContext2 } from "./context";
import { useState } from "react";
function App() {
return (
<MyContext.Provider value={{ name: '张三' }}>
<MyContext2.Provider value={{ age: 18 }}>
<SubComponent />
</MyContext2.Provider>
</MyContext.Provider>
);
}
export default App;
消费数据:
js
// Child3.jsx
import { useContext } from 'react';
import { MyContext, MyContext2 } from "../context"
export default function ChildCom4() {
const { name } = useContext(MyContext);
const { age } = useContext(MyContext2);
return (
<div>name:{name},age: {age}</div>
);
}
如果是使用Consumer组件消费数据,需要进行嵌套。
jsx
<MyContext.Consumer>
{(context) => {
return (
<MyContext2.Consumer>
{(context) => (
<div>
{/* 如果两个Provider传入了同样的变量名name, 这里的context取到的是最近的context中的数据 */}
<div>name:{context.name}</div>
</div>
)}
</MyContext2.Consumer>
)
}}
</MyContext.Consumer>
2. Context设置默认值
上文在在创建context时提到,createContext可以传入参数作为默认值。虽然好像不是很有用,因为它是不变的。
js
const MyContext = React.createContext(defaultValue);
注意:只有 当组件所处的树中没有匹配到 Provider 时,其 defaultValue
参数才会生效。此默认值有助于在不使用 Provider 包装组件 的情况下对组件进行测试。注意:将 undefined
传递给 Provider 的 value 时,消费组件的 defaultValue
不会生效。
还是用刚才的例子演示:
js
const MyContext = React.createContext({
name: "zhangsan",
age: 18,
});
不需要Provider提供数据:
js
// App.jsx
import SubComponent from "./components/SubComponent";
import { MyContext } from "./context";
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return <SubComponent />
}
export default App;
// SubComponent.jsx
import React from 'react';
import Child1 from "./Child1";
export default function SubComponent() {
return (
<div>
SubComponent
<Child1 />
</div>
);
}
使用数据:
js
import { useContext } from 'react';
import { MyContext } from "../context"
function ChildCom4() {
const { name, age } = useContext(MyContext);
return (
<div style={{ border: '1px solid', width: "200px"}} >
Child1
<div>name:{name}</div>
<div>age:{age}</div>
</div>
);
}
export default Child1;
运行,可以看到是能得到数据的。
注意事项
由上文可见,context看起来很强大,但是我们还是要避免过度使用context。
- 适度使用 :Context主要适用于全局数据或多个组件需要共享的状态管理,而不是用来替代组件通信的所有 props。在使用 context 之前,先试试传递 props 或者将 JSX 作为
children
传递。 - 性能考虑:Context 的更新会触发依赖它的所有组件重新渲染,因此在设计时要注意避免不必要的 Context 更新。
- 结构清晰:合理组织 Context 的提供者和消费者,确保代码结构清晰易懂,避免 Context 的嵌套过深或使用过于复杂。
context的使用场景
-
全局数据管理:当多个组件需要访问相同的数据时,可以将这些数据放在 Context 中,所有子组件可以直接访问这些数据,而不需要通过 props 层层传递。
-
主题设置:如果你的应用需要支持多种主题(比如暗黑模式和正常模式),可以将当前主题状态存储在 Context 中,然后所有受主题影响的组件可以订阅这个 Context。
-
用户认证状态:当需要在多个地方显示用户的认证状态(比如登录状态),可以将用户认证信息存储在 Context 中,然后所有需要显示或根据用户状态变化而变化的组件都可以从 Context 中读取这些信息。
-
多语言支持:如果你的应用需要多语言支持,可以将当前语言设置存储在 Context 中,然后所有需要显示文本的组件都可以从 Context 中获取当前语言的文本。
-
数据缓存:在某些情况下,如果你需要缓存一些数据以供应用的不同部分使用,可以使用 Context 来存储这些数据,以确保应用中各个组件共享相同的缓存数据。
当然,在我们的应用中,一般会结合使用Context和其他状态管理解决方案,如Redux或MobX,以满足不同的需求。