React Context API 简化 – 实用指南(更新)
使用 React Context API 管理您的应用程序数据很有趣并且易于实现!
然而,如果处理不当,它也可能成为一场噩梦,尤其是当您的应用程序规模不断扩大时。
作为一名 React 开发者,在 React 应用中传递数据是必不可少的。如果你刚开始学习 React,你很可能会进行所谓的“prop 钻取”。
这与手动通过 props 将数据从组件 A 传递到组件 C 和 D 有关。其中组件 A 是 C 和 D 的共同父级。
如果您处理的是一个简单的应用程序,那么这没问题。
但是随着应用规模的扩大,你会发现将这些数据向下传递到多个组件(从父组件到深层嵌套的子组件)不再有趣。你可能会看到一些组件仅仅充当传递这些数据的路径。在这种情况下,数据与组件本身无关。
为了解决这个问题,React 为我们提供了 Context API。
什么是 React context API
React 中的上下文 API 允许组件树中的每个组件都可以访问使用数据,而无需手动将 props 传递给它。
这样做的好处是,你可以省去“中间人”组件。这意味着只有需要数据的组件才会知道。
它的工作原理如下。
您将拥有一个存放全局数据的地方(例如一个 store)。然后,您将设置逻辑来公开这些数据,以便任何组件都可以轻松访问和检索它。
让我们看看如何实际实现这一点。
我们将处理一个项目(在本例中是一个简单的 Todos 应用程序),其中我们有全局数据(在父组件中),可以通过 props 由其子组件访问。
在本 React context 教程中,你将学习如何使用 context API 管理这些数据。你还将了解使用 React context 的陷阱,以及为什么不应该过早使用它。
准备项目文件
我们首先从这个 GitHub 仓库中提取我们的起始项目文件。然后从终端运行以下命令来克隆项目:
git clone https://github.com/Ibaslogic/simple-todo-app
这将以项目文件夹的名称创建一个目录。
引导项目文件和文件夹后,使用文本编辑器打开它并运行:
npm install
在运行上述命令之前,请确保您位于项目目录中。
该命令将在本地node_modules
文件夹中安装所有必需的依赖项。之后,运行以下命令启动开发服务器:
npm start
您应该在浏览器地址栏中的localhost:3000看到该应用程序
你可以按照本 React 教程指南学习如何从头构建此应用。其中,我们使用“props 钻孔”来实现数据流。
上图清晰地展示了组件的层次结构。正如预期的那样,你应该知道应用程序的组件文件位于该src/components
文件夹中。
是TodoContainer
包含所有其他子组件的父组件。其文件保存了子组件通过 props 访问的 todo 数据。
同样,它有几个类方法也需要访问待办事项状态数据。
如果你看过本教程或熟悉 React,你应该知道为什么我们要将状态提升到父组件。重申一下,对于每个访问状态数据的组件,state
对象都会在其最近的公共父级文件中声明。
我们做的就是所谓的“状态提升”!没什么特别的,就是基本的 React 功能。
现在,从组件树中,你可以推断出我们只有两层传递数据。从TodosContainer
组件到TodosList
,再到TodosItem
。
在这种情况下,最好/建议通过 props 手动传递数据。
但是如果你发现 prop 钻取成了问题——例如,你通过 props 向许多嵌套组件传递数据,导致某些组件仅用作路由。那么使用 Context API 会更好。
为了查看上下文 API 的实际作用,我们仍将使用这个 Todos 应用程序。
但请记住,如果道具钻孔成为一个问题,那么上下文是可取的(这个应用程序不是这种情况)。
再次强调,你不应该过早地寻求背景。
您将在本指南的后面了解原因。继续阅读!
设置上下文
正如我之前提到的,我们将创建一个中央存储库来存储我们的全局数据。因此,让我们context.js
在src
文件夹中创建一个名为 的新文件。在此文件中,添加以下起始代码:
import React, { Component } from "react"
const TodosContext = React.createContext()
const TodosProvider = TodosContext.Provider
// const TodosConsumer = TodosContext.Consumer
class MyContext extends Component {
render() {
return (
<TodosProvider value={"todos data"}>{this.props.children}</TodosProvider>
)
}
}
export { TodosContext, MyContext }
接下来,进入文件内部,用上下文组件src/index.js
包裹父组件。确保导入了上下文文件。TodoContainer
MyContext
...
import { MyContext } from "./context";
ReactDOM.render(
<MyContext>
<TodoContainer />
</MyContext>,
document.getElementById("root")
);
保存文件。
怎么了?
在上下文文件中,我们首先创建一个上下文对象并将其赋值给TodosContext
变量。在这里,您可以传递一个默认的上下文值,或者像上面那样直接传递一个空值。
现在,你可能会想:“为什么要使用类组件?这是 20XX 年,为什么不在函数组件中使用 Hook”。
无论组件类型(类或函数)如何,创建上下文对象的方法都是相同的。
此外,重点在于 Context API,而不是组件类型。另外,请记住,仍有人使用类组件。
您仍然不想使用任何类组件吗?
我已经介绍了如何使用 React Hook 仅使用函数组件编写相同的 Todos 应用。您可以快速浏览一遍,然后回来继续本教程。
由你决定!
继续。
一旦拥有了这个上下文对象,你就可以访问两个组件——Provider
和Consumer
。React Context Provider 允许树中的所有组件访问并使用上下文数据。
但直到您将需要访问此数据的组件或其共同父级(在我们的例子中是TodoContainer
)包装起来为止。
这告诉您,您还可以将提供程序包装在文件中的组件周围TodoContainer.js
。
children
作为 React 开发人员,您应该知道我们为什么在文件中使用prop context.js
。
回顾一下,组件this.props.children
中使用的 as是在文件中的标签MyContext
之间传递的 JSX/component – 即。<MyContext></MyContext>
index.js
<TodoContainer />
Provider
如上下文文件所示,它接受一个prop value
,用于存放所有数据。目前,我们传递的是一个简单的字符串。稍后,我们将传递一个完整的对象。
至此,我们的应用程序没有任何变化!
让我们看看如何从任何子组件访问/使用上下文值。
访问上下文数据
根据组件的类型,访问上下文数据的方式也有所不同。我们将首先介绍如何在类组件中访问这些数据。之后,您将学习如何在函数组件中以及通过 React Hook 实现相同的功能。
别忘了,就像 一样Provider
,我们也可以访问Consumer
。但目前,我们已将其注释掉,如context.js
文件中所示。当我们需要在函数组件中访问数据时,我们会用到它。
在类组件中访问上下文数据(使用 contextType)
打开src/components/TodosList.js
文件并导入上下文对象,TodosContext
如下所示:
import { TodosContext } from "../context"
在方法上方添加render()
:
static contextType = TodosContext;
这位于render()
方法内部但在return
语句上方。
const value = this.context
console.log(value)
保存文件并检查 DevTools 的控制台。
value
如您所见,我们正在此组件中接收分配给 prop(在上下文文件中)的数据TodosList
。
刚才发生了什么?
在代码中,我们首先contextType
使用static
类初始化 。然后将之前创建的 context 对象赋值给它。这样,我们就可以通过 访问value
了this.context
。
目前,我们向value
prop 传递了一个简单的字符串。不过,我们将传递state
应用程序对象中的全部待办事项数据。
因此现在,state
从TodoContainer
组件复制对象并将其粘贴到文件render()
中的方法上方context.js
。
注意:请暂时复制以避免分页。我们稍后会删除。
所以你有:
...
import { v4 as uuidv4 } from "uuid";
...
class MyContext extends Component {
state = {
todos: [
{
id: uuidv4(),
title: "Setup development environment",
completed: true,
},
{
id: uuidv4(),
title: "Develop website and add content",
completed: false,
},
{
id: uuidv4(),
title: "Deploy to live server",
completed: false,
},
],
};
render() {
return (
<TodosProvider value={{...this.state}}>
{this.props.children}
</TodosProvider>
);
}
}
...
记得更新value
中的道具<TodosProvider>
。
如果您保存文件并再次检查控制台,您将看到待办事项数据。
在value
prop 中,我们现在传递使用扩展运算符获取的整个 todos 状态数据…this.state
。
现在,value
prop 有了这些数据,它可以被树中的任何子组件使用。
接下来,我们将文件中的所有类方法TodoContainer.js
也移动到 中,context.js
使其能够全局访问。将它们剪切并粘贴到render()
方法上方。
现在,我们可以公开这些方法(就像我们对对象所做的那样state
),以便树中的其他组件可以访问。
因此,更新value
Provider 组件中的 prop 以包含这些方法,如下所示:
...
render() {
return (
<TodosProvider
value={{
...this.state,
handleChange: this.handleChange,
delTodo: this.delTodo,
addTodoItem: this.addTodoItem,
}}
>
{this.props.children}
</TodosProvider>
);
}
...
现在您可以删除文件state
中的对象TodoContainer.js
(记住我们将其移动到context.js
文件中)并删除props
与所有组件标签相关的所有内容。
您的TodoContainer.js
文件现在应如下所示:
import React from "react"
import TodosList from "./TodosList"
import Header from "./Header"
import InputTodo from "./InputTodo"
class TodoContainer extends React.Component {
render() {
return (
<div className="container">
<Header />
<InputTodo />
<TodosList />
</div>
)
}
}
export default TodoContainer
如你所见,我们不再需要props
将数据传递到各个子组件。现在所有组件都可以直接使用context.js
文件中的数据。
现在,我们有了更清晰的代码。
如果你保存文件并检查前端,你会看到一个分页符。这是因为该TodoList
组件仍在引用其父组件来获取待办事项数据。
修复这个问题很简单。
如你所知,此组件中的数据可以通过上下文获取。你所要做的就是指向数据保存的位置并获取它。
因此修改TodosList.js
文件,以便获得:
import React from "react"
import TodoItem from "./TodoItem"
import { TodosContext } from "../context"
class TodosList extends React.Component {
static contextType = TodosContext
render() {
const value = this.context
return (
<div>
{value.todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
)
}
}
export default TodosList
由于待办事项数据保存在变量中value
,我们按预期访问并循环遍历它。注意,<TodoItem />
此文件中的实例不再充当传递数据的路径。
保存文件并检查前端。你应该能看到你的应用渲染出来了。
就这么简单。
现在你已经知道如何在类组件中访问 context 数据了。这个应用还有很多地方需要修复。不过,我们已经取得了一些进展。
在函数组件中访问 Context 数据
随着React Hooks 的引入,你现在可以只使用函数组件来构建整个应用组件。因此,了解如何在此组件类型中访问这些数据至关重要。
如你所知,此应用仅使用类组件构建。尽管我已经介绍了如何使用函数组件构建它。如果你需要复习一下,可以快速浏览一下。
这告诉您,我们需要将其中一个组件转换为函数类型,以了解如何访问上下文数据。
这应该是直截了当的。
打开src/components/TodoItem.js
文件并用该函数组件替换类组件。
import React from "react"
const TodoItem = props => {
const completedStyle = {
fontStyle: "italic",
color: "#d35e0f",
opacity: 0.4,
textDecoration: "line-through",
}
const { completed, id, title } = props.todo
return (
<li className="todo-item">
<input
type="checkbox"
checked={completed}
onChange={() => props.handleChangeProps(id)}
/>
<button onClick={() => props.deleteTodoProps(id)}>Delete</button>
<span style={completed ? completedStyle : null}>{title}</span>
</li>
)
}
export default TodoItem
转换就是这样。如果您还在摸索,请快速浏览最后一个链接,学习如何进行转换。
现在让我们看看如何访问此组件中的上下文数据。
如果你查看此文件中的onChange
和事件处理程序,就会发现我们通过 props 访问了父组件的和方法。现在,我们在文件中提供了这些方法。onClick
handleChange
delTodo
context.js
让我们访问它们。
由于我们处理的是函数组件,之前在类组件中使用的方法已不再适用。相反,我们将使用Consumer
组件。该组件允许我们在函数组件中访问 Context 数据。
请记住,此Consumer
组件存在于上下文文件中。
因此进入文件并取消注释此行。
// const TodosConsumer = TodosContext.Consumer
然后,更新export
以将其包含如下内容:
export { TodosContext, MyContext, TodosConsumer }
保存文件。
返回文件,从上下文文件TodoItem.js
导入。TodosConsumer
import { TodosConsumer } from "../context"
然后,更新return
语句,以便:
...
return (
<TodosConsumer>
{(value) => {
console.log(value);
return (
<li className="todo-item">
<input
type="checkbox"
checked={completed}
onChange={() => props.handleChangeProps(id)}
/>
<button onClick={() => props.deleteTodoProps(id)}>Delete</button>
<span style={completed ? completedStyle : null}>{title}</span>
</li>
);
}}
</TodosConsumer>
);
...
这种访问 context 数据的方法使用了所谓的render prop。你无需学习也能使用它。
它非常简单和直接。
它TodosConsumer
需要一个接受参数的函数作为子函数value
。该函数保存了分配给Provider 属性的value
所有上下文对象。value
让我们保存文件并检查控制台以查看其内容(因为我们正在代码中value
记录)。value
现在我们已经获取了数据,让我们更新return
语句来使用它们。
return (
<TodosConsumer>
{value => {
const { handleChange, delTodo } = value
return (
<li className="todo-item">
<input
type="checkbox"
checked={completed}
onChange={() => handleChange(id)}
/>
<button onClick={() => delTodo(id)}>Delete</button>
<span style={completed ? completedStyle : null}>{title}</span>
</li>
)
}}
</TodosConsumer>
)
通过 JavaScript 对象解构,我们从参数中提取handleChange
和方法。delTodo
value
然后我们分别用props.handleChangeProps
和替换和。props.deleteTodoProps
handleChange
delTodo
保存文件。
现在,您知道如何访问函数组件中的上下文数据。
您应该能够切换复选框并删除待办事项。但您还无法提交待办事项。我们会逐步解决。
继续。
使用 Hook 访问 React 上下文
这里我们也将使用函数组件。但这次,我们将使用一种更简单的方法——Hook 方法。
这是我喜欢的方法。非常简单明了。
我们将看一下控制输入字段和提交的组件。所以打开InputTodo.js
文件。不幸的是,这个组件是基于类的。这意味着我们需要将其转换为函数才能使用 Hook。
如果您遵循我的 React Hook 教程,那么这种转换应该是小菜一碟。
给你:
import React, { useState } from "react"
const InputTodo = props => {
const [inputText, setInputText] = useState({
title: "",
})
const onChange = e => {
setInputText({
...inputText,
[e.target.name]: e.target.value,
})
}
const handleSubmit = e => {
e.preventDefault()
props.addTodoProps(inputText.title)
setInputText({
title: "",
})
}
return (
<form onSubmit={handleSubmit} className="form-container">
<input
type="text"
className="input-text"
placeholder="Add todo..."
value={inputText.title}
name="title"
onChange={onChange}
/>
<input type="submit" className="input-submit" value="Submit" />
</form>
)
}
export default InputTodo
好的。
现在,让我们更新InputTodo.js
文件以使用上下文。
您应该:
import React, { useState, useContext } from "react"
import { TodosContext } from "../context";
const InputTodo = props => {
const value = useContext(TodosContext);
const { addTodoItem } = value;
...
const handleSubmit = e => {
e.preventDefault()
addTodoItem(inputText.title)
setInputText({
title: "",
})
}
return (
...
)
}
export default InputTodo
注意我们是如何修改handleSubmit
函数的。我们不再像之前指定的那样通过 props 传递addTodoItem
方法,而是直接从上下文中调用该方法。
React 提供了useContext
Hook 来读取函数组件中的 context 数据。你需要做的就是将 context 对象作为参数传递。
这很简单。如你所见,只需一行代码,我们就准备好了可用的上下文数据。然后,我们通过解构来获取函数addTodoItem
中所需的数据。handleSubmit
value
提示:如果您想访问类方法之外的上下文,请使用函数组件
render()
。
您可以保存文件并测试您的应用程序。
它应该能按预期工作。
React Context API 的性能问题
React context 以重新渲染组件而闻名,这些组件在每次value
上下文中的 prop 发生变化时都会使用上下文数据。
我的意思是什么?
每当value
上下文发生变化时,触发更改的消费者组件都会重新渲染以获取更新的值。这样就没问题了。
但重新渲染并不仅限于该消费者组件,而是访问上下文的所有组件。
虽然这可能不会对小型应用程序造成太大的性能问题,但当您的应用程序变得复杂时,这一点不容忽视。
让我们在我们的应用程序中看看这些问题。
我们将在所有组件文件中记录一些文本。
从src/components/TodoContainer.js
文件开始。在语句上方添加以下内容return
:
console.log("TodoContainer is running")
转到src/components/Header.js
文件并添加上述return
声明:
console.log("Header is running")
在该src/components/InputTodo.js
文件中,还添加以下内容:
console.log("InputTodo is running", addTodoItem)
在 中src/components/TodosList.js
添加以下内容:
console.log("TodosList is running", value)
最后,在TodoItem.js
文件中添加日志。
...
return (
<TodosConsumer>
{(value) => {
const { handleChange, delTodo } = value;
console.log("TodoItem is running", handleChange, delTodo);
return (
...
);
}}
</TodosConsumer>
);
...
保存所有文件并查看浏览器 DevTools 的控制台。
如上所示,
在页面加载时,所有组件都会呈现并在控制台中显示各自的日志消息(如上图中红色边框突出显示的那样)。
如果您点击任何复选框、删除或提交按钮,所有使用上下文数据的组件都将重新渲染(如黑色边框中突出显示的那样)。即使这些单独的元素正在访问部分数据。
这就是将对象传递给上下文value
而不是简单的字符串或数字的危险之处。一个只影响对象一部分的简单更新就会导致无数次组件重新渲染。
从上图可以看出,TodoContainer
组件Header
在初始页面加载后没有重新渲染。这是因为它们没有使用 context 数据。
现在,让我们尝试在控制台打开时在文本输入字段中写一些内容。
每次击键时,仅InputTodo.js
呈现。
发生这种情况是因为onChange
该组件中的函数(通过本地状态变量负责这些更改)不是上下文的一部分。
想象一下,你把这个onChange
函数和本地状态传递给 context value
prop。你认为会发生什么?
每次按键时,所有使用上下文数据的组件都会重新渲染。这并不理想,因为可能会导致性能问题。
这里需要注意一点:
应用中的状态数据并非都需要全局访问(即放置在 context 中)。请将本地状态保留在需要的地方。
从目前的情况来看,
如果您想无缝避免不必要的组件重新渲染的问题,那么上下文可能实际上并不适合状态频繁变化的应用程序。
虽然,我们可以通过将上下文拆分成多个来解决这个问题。但在这种情况下,上下文数据的不同部分应该能够独立更新。
结论
虽然您已经了解了如何在 React 应用程序中使用上下文 API,而不管组件类型如何,但您也看到了此 API 带来的常见陷阱。
虽然许多开发人员认为,即使在复杂的应用中,只要没有出现性能问题,使用 Snapshot 也是安全的。但我们不能忽视组件中无数次的重新渲染。
话虽如此,我建议您仅当您的状态数据需要低频率更新并且当您发现 prop 钻探正在成为一个问题时(即当您将 props 传递到许多深层嵌套的组件中时)才使用上下文 API。
不要仅仅因为想要避免道具钻孔而使用它(如果这是非常可行的)。
现在轮到你了!
关于这个主题,你还有什么疑问、困难或建议吗?请在评论区留言。
如果您喜欢这个 React 上下文教程,请努力在网络上分享这篇文章,并确保在Twitter上关注我以接收更多更新。
Twitter:@ibaslogic。