React Context和useContext()hook的指南
React上下文为组件提供数据,无论它们在组件树中有多深。上下文用于管理全局数据,例如全局状态、主题、服务、用户设置等。
在这篇文章中,你将学习如何在React中使用上下文概念。
目录
1.如何使用上下文
在React中使用上下文需要3个简单的步骤:创建上下文,提供上下文,以及消费上下文。
A.创建上下文
内置的工厂函数createContext(default) ,创建一个上下文实例。
import { createContext } from 'react';
const Context = createContext('Default Value');
该工厂函数接受一个可选的参数:默认值。
B.提供上下文
Context.Provider 上下文实例上可用的组件被用来向其子组件提供上下文,无论它们有多深。
要设置context的值,请使用<Context.Provider value={value} /> 上可用的道具value :
function Main() {
const value = 'My Context Value';
return (
<Context.Provider value={value}>
<MyComponent />
</Context.Provider>
);
}
同样,这里重要的是,所有后来想消费上下文的组件都必须被包裹在提供者组件内。
如果你想改变上下文的值,只需更新value prop。
C.消耗上下文
消耗上下文可以通过两种方式进行。
第一种方式,也是我推荐的方式,是使用useContext(Context) React挂钩。
import { useContext } from 'react';
function MyComponent() {
const value = useContext(Context);
return <span>{value}</span>;
}
该钩子返回上下文的值:value = useContext(Context) 。该钩子还确保在上下文值改变时重新渲染组件。
第二种方式是通过使用作为子节点提供的渲染函数来Context.Consumer 上下文实例中可用的特殊组件。
function MyComponent() {
return (
<Context.Consumer>
{value => <span>{value}</span>}
</Context.Consumer>
);
}
同样,如果上下文值发生变化,<Context.Consumer> 将重新渲染其渲染函数。
你可以为一个单一的上下文拥有你想要的多个消费者。如果上下文值发生变化(通过改变提供者<Context.Provider value={value} /> 的道具value ),那么所有的消费者都会被立即通知并重新渲染。
如果消费者没有被包裹在提供者里面,但仍然试图访问上下文的值(使用useContext(Context) 或<Context.Consumer> ),那么上下文的值将是提供给创建上下文的createContext(defaultValue) 工厂函数的默认值参数。
2.什么时候需要上下文?
使用上下文的主要想法是允许你的组件访问一些全局数据,并在全局数据发生变化时重新渲染。上下文解决了道具钻取的问题:当你必须把道具从父辈传给子辈时。
你可以在上下文里面持有:
- 全局状态
- 主题
- 应用程序配置
- 认证的用户名称
- 用户设置
- 首选语言
- 服务集合
在另一方面,在决定在你的应用程序中使用上下文之前,你应该仔细考虑。
首先,整合上下文会增加复杂性。创建上下文,在提供者中包装一切,在每个消费者中使用useContext() - 这增加了复杂性。
其次,添加上下文使得单元测试组件更加困难。在单元测试期间,你必须将消费者组件包裹到上下文提供者中。包括那些间接受到上下文影响的组件--上下文消费者的祖先
3.用例:全局用户名称
将数据从父级组件传递给子级组件的最简单的方法是父级组件向其子级组件分配道具。
function Application() {
const userName = "John Smith";
return <UserInfo userName={userName} />;
}
function UserInfo({ userName }) {
return <span>{userName}</span>;
}
父组件<Application /> 使用userName 道具将userName 数据分配给其子组件<UserInfo name={userName} /> 。
这就是使用props传递数据的通常方式。你可以毫无问题地使用这种方法。
当<UserInfo /> 子组件不是<Application /> 的直接子组件,而是包含在多个祖先中时,情况就会改变。
例如,假设<Application /> 组件(拥有全局数据的userName )演绎了<Layout /> 组件,后者又演绎了<Header /> 组件,后者最后又演绎了<UserInfo /> 组件(想访问userName )。
下面是这种结构的样子:
function Application() {
const userName = "John Smith";
return (
<Layout userName={userName}>
Main content
</Layout>
);
}
function Layout({ children, userName }) {
return (
<div>
<Header userName={userName} />
<main>
{children}
</main>
</div>
)
}
function Header({ userName }) {
return (
<header>
<UserInfo userName={userName} />
</header>
);
}
function UserInfo({ userName }) {
return <span>{userName}</span>;
}
你可以看到问题所在:因为<UserInfo /> 组件在树的深处渲染,而所有的父组件(<Layout /> 和<Header /> )都必须通过userName 道具。
这个问题也被称为道具钻孔。
React上下文是一个可能的解决方案。让我们在下一节看看如何应用它。
3.1 上下文的拯救
作为快速提醒,应用React上下文需要3个角色:上下文、从上下文中提取的提供者和消费者。
下面是将上下文应用于示例应用程序时的情况。
import { useContext, createContext } from 'react';
const UserContext = createContext('Unknown');
function Application() {
const userName = "John Smith";
return (
<UserContext.Provider value={userName}>
<Layout>
Main content
</Layout>
</UserContext.Provider>
);
}
function Layout({ children }) {
return (
<div>
<Header />
<main>
{children}
</main>
</div>
);
}
function Header() {
return (
<header>
<UserInfo />
</header>
);
}
function UserInfo() {
const userName = useContext(UserContext);
return <span>{userName}</span>;
}
让我们更详细地看看已经做了什么。
首先,const UserContext = createContext('Unknown') 创建了将保存用户名信息的上下文。
其次,在<Application /> 组件中,应用程序的子组件被包裹在用户上下文提供者中。<UserContext.Provider value={userName}>.注意,提供者组件的value 道具很重要:这是你如何设置上下文的值。
最后,通过使用内置的useContext(UserContext) 钩子,<UserInfo /> 成为上下文的消费者。该钩子以上下文为参数被调用,并返回用户名的值。
<Layout /> 和 <Header />,中间组件不需要传下 userName 的道具。这就是上下文的最大好处:它消除了通过中间组件传递数据的负担。
3.2 上下文改变时
当情境值通过改变情境提供者(<Context.Provider value={value} />)的value prop而改变时,那么其所有的消费者都会被通知并重新渲染。
例如,如果我把用户名从'John Smith' 改为'Smith, John Smith' ,那么<UserInfo /> 消费者立即重新渲染以显示最新的上下文值。
import { createContext, useEffect, useState } from 'react';
const UserContext = createContext('Unknown');
function Application() {
const [userName, setUserName] = useState('John Smith');
useEffect(() => {
setTimeout(() => {
setUserName('Smith, John Smith');
}, 2000);
}, []);
return (
<UserContext.Provider value={userName}>
<Layout>
Main content
</Layout>
</UserContext.Provider>
);
}
// ...
打开演示,你会看到'John Smith' (背景值)显示在屏幕上。2秒后,上下文值变为'Smith, John Smith' ,相应地,屏幕上也会更新新的值。
该演示表明,<UserInfo /> 组件,即在屏幕上渲染上下文值的消费者,在上下文值发生变化时重新渲染。
function UserInfo() {
const userName = useContext(UserContext);
return <span>{userName}</span>;
}
4.更新上下文
React Context API默认是无状态的,没有提供专门的方法来更新消费者组件的上下文值。
但这可以通过集成一个状态管理机制(如useState() 或useReducer() 钩子),并在上下文中紧挨着值本身提供一个更新函数而轻松实现。
在下面的例子中,<Application /> 组件使用useState() 钩子来管理上下文的值。
import { createContext, useState, useContext, useMemo } from 'react';
const UserContext = createContext({
userName: '',
setUserName: () => {},
});
function Application() {
const [userName, setUserName] = useState('John Smith');
const value = useMemo(
() => ({ userName, setUserName }),
[userName]
);
return (
<UserContext.Provider value={value}>
<UserNameInput />
<UserInfo />
</UserContext.Provider>
);
}
function UserNameInput() {
const { userName, setUserName } = useContext(UserContext);
const changeHandler = event => setUserName(event.target.value);
return (
<input
type="text"
value={userName}
onChange={changeHandler}
/>
);
}
function UserInfo() {
const { userName } = useContext(UserContext);
return <span>{userName}</span>;
}
<UserNameInput /> 消费者读取上下文值,从那里提取 和 。然后消费者可以通过调用更新函数 来更新上下文值。userName setUserName setUserName(newContextValue)
<UserInfo /> 是该上下文的另一个消费者。当<UserNameInput /> 更新上下文时,这个组件也会被更新。
请注意,<Application /> 对上下文值进行备忘。只要userName ,备忘化就能使上下文值对象保持不变,防止每次<Application /> 重新渲染消费者时都要重新渲染。
否则,如果没有记忆化,const value = { userName, setUserName } 会在重新渲染<Application /> 的过程中创建不同的对象实例,从而引发上下文消费者的重新渲染。请看更多关于对象的参考性平等。
5.总结
React中的上下文是一个概念,它允许你为子组件提供全局数据,无论它们在组件树中的位置有多深。
使用上下文需要3个步骤:创建、提供和消费上下文。
当把上下文集成到你的应用程序中时,要考虑到它会增加相当多的复杂性。有时在层次结构中钻研道具的2-3层并不是一个大问题。
你知道React上下文的哪些用例?