作者选择Creative Commons接受捐赠,作为Write for DOnations计划的一部分。
介绍
在本教程中,您将使用React context在多个组件之间共享状态。React 上下文是一个接口,用于与其他组件共享信息,而无需显式地将数据作为props传递。这意味着您可以在父组件和深度嵌套的子组件之间共享信息,或者将站点范围的数据存储在一个位置并在应用程序的任何位置访问它们。您甚至可以通过随数据一起提供更新功能来更新嵌套组件中的数据。
React 上下文足够灵活,可以用作项目的集中状态管理系统,或者您可以将其范围用于应用程序的较小部分。借助上下文,您可以在整个应用程序中共享数据,无需任何额外的第三方工具,只需进行少量配置。这为Redux 等工具提供了更轻量级的替代方案,后者可以帮助处理更大的应用程序,但对于中型项目可能需要太多设置。
在本教程中,您将使用上下文来构建使用跨不同组件的公共数据集的应用程序。为了说明这一点,您将创建一个网站,用户可以在其中制作自定义沙拉。该网站将使用上下文来存储客户信息、喜爱的物品和定制沙拉。然后,您将访问该数据并在整个应用程序中更新它,而无需通过 props 传递数据。在本教程结束时,您将学习如何使用上下文在项目的不同级别存储数据,以及如何访问和更新嵌套组件中的数据。
先决条件
-
你需要一个运行Node.js的开发环境;本教程在 Node.js 版本 10.20.1 和 npm 版本 6.14.4 上进行了测试。要在 macOS 或 Ubuntu 18.04 上安装它,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。
-
使用Create React App设置的 React 开发环境,删除了非必要的样板。要进行设置,请按照如何管理 React 类组件上的状态教程的步骤 1 — 创建空项目进行操作。本教程将
state-context-tutorial
用作项目名称。 -
您还需要具备 JavaScript 的基本知识(可以在How To Code in JavaScript 中找到),以及 HTML 和 CSS 的基本知识。有关 HTML 和 CSS 的有用资源是Mozilla Developer Network。
-
您将使用 React 组件、
useState
Hook 和useReducer
Hook,您可以在我们的教程如何在 React 中创建自定义组件和如何在 React 组件上使用钩子管理状态中了解这些内容。
第 1 步 — 为您的应用程序奠定基础
在此步骤中,您将构建自定义沙拉生成器的一般结构。您将创建组件以显示可能的浇头、选定的浇头列表和客户信息。当您使用静态数据构建应用程序时,您会发现如何在各种组件中使用不同的信息片段,以及如何识别在上下文中有用的数据片段。
以下是您将构建的应用程序示例:
请注意您可能需要跨组件使用的信息。例如,用户名(在本示例中为Kwame)在导航区域中显示用户数据,但您可能还需要用户信息来标识最喜欢的项目或结帐页面。应用程序中的任何组件都需要可以访问用户信息。查看沙拉生成器本身,每种沙拉成分都需要能够更新屏幕底部的“您的沙拉”列表,因此您还需要从每个组件都可以访问的位置存储和更新该数据.
首先对所有数据进行硬编码,以便您可以计算出应用程序的结构。稍后,您将在下一步开始添加上下文。随着应用程序开始增长,上下文提供了最大的价值,因此在此步骤中,您将构建多个组件来展示上下文如何跨组件树工作。对于较小的组件或库,您通常可以使用包装组件和较低级别的状态管理技术,例如React Hooks和基于类的管理。
由于您正在构建具有多个组件的小型应用程序,因此请安装JSS以确保不会有任何类名冲突,以便您可以在与组件相同的文件中添加样式。有关 JSS 的更多信息,请参阅样式化 React 组件。
运行以下命令:
- npm install react-jss
npm 将安装该组件,完成后您将看到如下消息:
Output+ react-jss@10.3.0
added 27 packages from 10 contributors, removed 10 packages andaudited 1973 packages in 15.507s
现在您已经安装了 JSS,请考虑您需要的不同组件。在页面顶部,您将有一个Navigation
用于存储欢迎消息的组件。下一个组件将是它SaladMaker
本身。这将包含标题以及底部的构建器和您的沙拉列表。包含成分的部分将是一个单独的组件,称为SaladBuilder
,嵌套在 内SaladMaker
。每个成分都是一个SaladItem
组件的实例。最后,底部列表将是一个名为 的组件SaladSummary
。
注意:组件不需要这样划分。在您处理应用程序时,您的结构会随着您添加更多功能而发生变化和发展。此示例旨在为您提供一个结构来探索上下文如何影响树中的不同组件。
既然您已经了解了需要的组件,请为每个组件创建一个目录:
- mkdir src/components/Navigation
- mkdir src/components/SaladMaker
- mkdir src/components/SaladItem
- mkdir src/components/SaladBuilder
- mkdir src/components/SaladSummary
接下来,从Navigation
. 首先,在文本编辑器中打开组件文件:
- nano src/components/Navigation/Navigation.js
创建一个名为的组件Navigation
并添加一些样式以提供Navigation
边框和填充:
import React from 'react';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
wrapper: {
borderBottom: 'black solid 1px',
padding: [15, 10],
textAlign: 'right',
}
});
export default function Navigation() {
const classes = useStyles();
return(
<div className={classes.wrapper}>
Welcome, Kwame
</div>
)
}
由于您使用的是 JSS,您可以直接在组件中而不是在 CSS 文件中创建样式对象。包装器div
将有一个填充、一个solid
black
边框,并将文本与textAlign
.
保存并关闭文件。接下来, open App.js
,这是项目的根:
- nano src/components/App/App.js
导入Navigation
组件并通过添加突出显示的行将其呈现在空标签中:
import React from 'react';
import Navigation from '../Navigation/Navigation';
function App() {
return (
<>
<Navigation />
</>
);
}
export default App;
保存并关闭文件。当您这样做时,浏览器将刷新,您将看到导航栏:
将导航栏视为一个全局组件,因为在此示例中,它用作模板组件,将在每个页面上重复使用。
下一个组件将是它SaladMaker
本身。这是一个只会在特定页面或特定状态下呈现的组件。
SaladMaker.js
在文本编辑器中打开:
- nano src/components/SaladMaker/SaladMaker.js
创建一个<h1>
带有标题标签的组件:
import React from 'react';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
wrapper: {
textAlign: 'center',
}
});
export default function SaladMaker() {
const classes = useStyles();
return(
<>
<h1 className={classes.wrapper}>
<span role="img" aria-label="salad">🥗 </span>
Build Your Custom Salad!
<span role="img" aria-label="salad"> 🥗</span>
</h1>
</>
)
}
在此代码中,您textAlign
用于将组件居中放置在页面上。元素的role
和aria-label
属性span
将有助于使用Accessible Rich Internet Applications (ARIA)进行可访问性。
保存并关闭文件。打开App.js
渲染组件:
- nano src/components/App/App.js
SaladMaker
在Navigation
组件之后导入和渲染:
import React from 'react';
import Navigation from '../Navigation/Navigation';
import SaladMaker from '../SaladMaker/SaladMaker';
function App() {
return (
<>
<Navigation />
<SaladMaker />
</>
);
}
export default App;
保存并关闭文件。当你这样做时,页面将重新加载,你会看到标题:
接下来,创建一个名为SaladItem
. 这将是每种成分的卡片。
在文本编辑器中打开文件:
- nano src/components/SaladItem/SaladItem.js
该组件将包含三个部分:项目名称、显示项目是否是用户最喜欢的图标,以及放置在按钮内的表情符号,该按钮将在单击时将该项目添加到沙拉中。将以下行添加到SaladItem.js
:
import React from 'react';
import PropTypes from 'prop-types';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
add: {
background: 'none',
border: 'none',
cursor: 'pointer',
},
favorite: {
fontSize: 20,
position: 'absolute',
top: 10,
right: 10,
},
image: {
fontSize: 80
},
wrapper: {
border: 'lightgrey solid 1px',
margin: 20,
padding: 25,
position: 'relative',
textAlign: 'center',
textTransform: 'capitalize',
width: 200,
}
});
export default function SaladItem({ image, name }) {
const classes = useStyles();
const favorite = true;
return(
<div className={classes.wrapper}>
<h3>
{name}
</h3>
<span className={classes.favorite} aria-label={favorite ? 'Favorite' : 'Not Favorite'}>
{favorite ? '😋' : ''}
</span>
<button className={classes.add}>
<span className={classes.image} role="img" aria-label={name}>{image}</span>
</button>
</div>
)
}
SaladItem.propTypes = {
image: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}
该image
和name
会的道具。代码使用favorite
变量和三元运算符来有条件地确定favorite
图标是否出现。该favorite
变量稍后将根据上下文作为用户配置文件的一部分来确定。现在,将其设置为true
. 样式会将收藏夹图标放置在卡片的右上角,并删除按钮上的默认边框和背景。该wrapper
课程将增加一个小的边框和改造一些文字。最后,PropTypes
添加了一个弱类型系统来提供一些强制措施,以确保不会传递错误的 prop 类型。
保存并关闭文件。现在,您需要渲染不同的项目。您将使用名为 的组件执行此操作,该组件SaladBuilder
将包含将转换为一系列SaladItem
组件的项目列表:
打开SaladBuilder
:
- nano src/components/SaladBuilder/SaladBuilder.js
如果这是一个生产应用程序,则此数据通常来自应用程序编程接口 (API)。但是现在,请使用硬编码的成分列表:
import React from 'react';
import SaladItem from '../SaladItem/SaladItem';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
wrapper: {
display: 'flex',
flexWrap: 'wrap',
padding: [10, 50],
justifyContent: 'center',
}
});
const ingredients = [
{
image: '🍎',
name: 'apple',
},
{
image: '🥑',
name: 'avocado',
},
{
image: '🥦',
name: 'broccoli',
},
{
image: '🥕',
name: 'carrot',
},
{
image: '🍷',
name: 'red wine dressing',
},
{
image: '🍚',
name: 'seasoned rice',
},
];
export default function SaladBuilder() {
const classes = useStyles();
return(
<div className={classes.wrapper}>
{
ingredients.map(ingredient => (
<SaladItem
key={ingredient.name}
image={ingredient.image}
name={ingredient.name}
/>
))
}
</div>
)
}
此代码段使用map()
数组方法映射列表中的每个项目,将name
和image
作为道具传递给SaladItem
组件。确保在映射时为每个项目添加key
。此组件的样式增加了的显示flex
为Flexbox的布局,包裹物的组分,和中心它们。
保存并关闭文件。
最后,渲染组件,SaladMaker
使其出现在页面中。
打开SaladMaker
:
- nano src/components/SaladMaker/SaladMaker.js
然后SaladBuilder
在标题后导入并渲染:
import React from 'react';
import { createUseStyles } from 'react-jss';
import SaladBuilder from '../SaladBuilder/SaladBuilder';
const useStyles = createUseStyles({
wrapper: {
textAlign: 'center',
}
});
export default function SaladMaker() {
const classes = useStyles();
return(
<>
<h1 className={classes.wrapper}>
<span role="img" aria-label="salad">🥗 </span>
Build Your Custom Salad!
<span role="img" aria-label="salad"> 🥗</span>
</h1>
<SaladBuilder />
</>
)
}
保存并关闭文件。当您这样做时,页面将重新加载,您将找到内容:
最后一步是添加正在进行的沙拉的摘要。此组件将显示用户选择的项目列表。现在,您将对项目进行硬编码。您将在步骤 3 中使用上下文更新它们。
SaladSummary
在文本编辑器中打开:
- nano src/components/SaladSummary/SaladSummary.js
该组件将是一个标题和一个未排序的项目列表。您将使用 flexbox 使它们包装:
import React from 'react';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
list: {
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
maxHeight: 50,
'& li': {
width: 100
}
},
wrapper: {
borderTop: 'black solid 1px',
display: 'flex',
padding: 25,
}
});
export default function SaladSummary() {
const classes = useStyles();
return(
<div className={classes.wrapper}>
<h2>Your Salad</h2>
<ul className={classes.list}>
<li>Apple</li>
<li>Avocado</li>
<li>Carrots</li>
</ul>
</div>
)
}
保存文件。然后打开SaladMaker
渲染项目:
- nano src/components/SaladMaker/SaladMaker.js
导入SaladSummary
后添加SaladBuilder
:
import React from 'react';
import { createUseStyles } from 'react-jss';
import SaladBuilder from '../SaladBuilder/SaladBuilder';
import SaladSummary from '../SaladSummary/SaladSummary';
const useStyles = createUseStyles({
wrapper: {
textAlign: 'center',
}
});
export default function SaladMaker() {
const classes = useStyles();
return(
<>
<h1 className={classes.wrapper}>
<span role="img" aria-label="salad">🥗 </span>
Build Your Custom Salad!
<span role="img" aria-label="salad"> 🥗</span>
</h1>
<SaladBuilder />
<SaladSummary />
</>
)
}
保存并关闭文件。当您这样做时,页面将刷新,您将找到完整的应用程序:
在整个应用程序中有共享数据。该Navigation
组件和SaladItem
组件都需要了解一下用户:他们的名字和他们的收藏列表。该SaladItem
还需要是在访问更新数据SaladSummary
的组成部分。这些组件共享共同的祖先,但通过树向下传递数据将是困难的并且容易出错。
这就是上下文的用武之地。您可以在公共父级中声明数据,然后稍后访问,而无需将其显式传递到组件的层次结构中。
在此步骤中,您创建了一个应用程序以允许用户从选项列表中构建沙拉。您创建了一组需要访问或更新由其他组件控制的数据的组件。在下一步中,您将使用上下文来存储数据并在子组件中访问它。
步骤 2 — 从根组件提供数据
在此步骤中,您将使用上下文将客户信息存储在组件的根目录中。您将创建一个自定义上下文,然后使用一个称为 a 的特殊包装组件Provider
,它将信息存储在项目的根目录中。然后,您将使用useContext
Hook与嵌套组件中的提供程序连接,以便您可以显示静态信息。在此步骤结束时,您将能够提供集中的信息存储并使用存储在许多不同组件的上下文中的信息。
最基本的上下文是用于共享信息的接口。许多应用程序都有一些需要在整个应用程序中共享的通用信息,例如用户首选项、主题信息和站点范围的应用程序更改。通过上下文,您可以将该信息存储在根级别,然后在任何地方访问它。由于您在父级中设置了信息,因此您知道它始终可用并且始终是最新的。
要添加上下文,请创建一个名为 的新目录User
:
- mkdir src/components/User
User
不会是一个传统的组件,因为你将把它用作一个组件和一个名为 .hook 的特殊 Hook 的数据useContext
。现在,保留平面文件结构,但如果您使用大量上下文,可能值得将它们移动到不同的目录结构。
接下来,User.js
在文本编辑器中打开:
- nano src/components/User/User.js
在文件中,createContext
从 React导入函数,然后执行函数并导出结果:
import { createContext } from 'react';
const UserContext = createContext();
export default UserContext;
通过执行该函数,您已经注册了上下文。结果 ,UserContext
就是您将在组件中使用的内容。
保存并关闭文件。
下一步是将上下文应用于一组元素。为此,您将使用一个名为 a 的组件Provider
。的Provider
是设定数据,然后包装一些子组件的组件。任何包裹子组件将有从访问数据Provider
与useContext
挂钩。
由于用户数据在整个项目中是不变的,因此尽可能将其放在组件树的高处。在此应用程序中,您将把它放在App
组件的根级别:
打开App
:
- nano src/components/App/App.js
添加以下突出显示的代码行以导入上下文并传递数据:
import React from 'react';
import Navigation from '../Navigation/Navigation';
import SaladMaker from '../SaladMaker/SaladMaker';
import UserContext from '../User/User';
const user = {
name: 'Kwame',
favorites: [
'avocado',
'carrot'
]
}
function App() {
return (
<UserContext.Provider value={user}>
<Navigation />
<SaladMaker />
</UserContext.Provider>
);
}
export default App;
在典型的应用程序中,您将获取用户数据或在服务器端渲染期间将其存储。在这种情况下,您对可能从 API 接收到的一些数据进行了硬编码。您创建了一个名为的对象user
,该对象将用户名保存为一个字符串和一组最喜欢的成分。
接下来,您导入了UserContext
,然后包装Navigation
并SaladMaker
使用了一个名为 的组件UserContext.Provider
。请注意在这种情况下如何UserContext
充当标准 React 组件。该组件将采用一个名为value
. 该道具将是您要共享的数据,在这种情况下是user
对象。
保存并关闭文件。现在,数据在整个应用程序中都可用。但是,要使用数据,您需要再次导入和访问上下文。
现在您已经设置了上下文,您可以开始用动态值替换组件中的硬编码数据。首先用Navigation
您设置的用户数据替换硬编码的名称UserContext.Provider
。
打开Navigation.js
:
- nano src/components/Navigation/Navigation.js
在 内部Navigation
,useContext
从 React 和UserContext
组件目录导入Hook 。然后调用useContext
usingUserContext
作为参数。与 不同的是UserContext.Provider
,您不需要UserContext
在JSX 中进行渲染。Hook 将返回您在value
prop 中提供的数据。将数据保存到一个名为 的新变量中user
,该变量是一个包含name
和的对象favorites
。然后,您可以将硬编码名称替换为user.name
:
import React, { useContext } from 'react';
import { createUseStyles } from 'react-jss';
import UserContext from '../User/User';
const useStyles = createUseStyles({
wrapper: {
outline: 'black solid 1px',
padding: [15, 10],
textAlign: 'right',
}
});
export default function Navigation() {
const user = useContext(UserContext);
const classes = useStyles();
return(
<div className={classes.wrapper}>
Welcome, {user.name}
</div>
)
}
UserContext
在 中作为组件工作App.js
,但在这里您更多地将它用作数据。但是,如果您愿意,它仍然可以充当组件。您可以使用Consumer
属于UserContext
. 您通过添加UserContext.Consumer
到 JSX 来检索数据,然后使用函数作为子项来访问数据。
虽然可以使用该Consumer
组件,但使用 Hooks 通常可以更短且更易于阅读,同时仍能提供相同的最新信息。这就是本教程使用 Hooks 方法的原因。
保存并关闭文件。当您这样做时,页面将刷新并且您将看到相同的名称。但是这次它动态更新了:
在这种情况下,数据没有跨许多组件传输。表示数据行进路径的组件树将如下所示:
| UserContext.Provider
| Navigation
您可以将此用户名作为道具传递,在这种规模下,这可能是一种有效的策略。但是随着应用程序的增长,Navigation
组件有可能会移动。可能有一个组件称为Header
包装Navigation
组件和另一个组件,例如 a TitleBar
,或者您可能会创建一个Template
组件,然后将 嵌套Navigation
在其中。通过使用上下文,Navigation
只要Provider
在树上,您就不必重构,从而使重构更容易。
下一个需要用户数据的SaladItem
组件是组件。在SaladItem
组件中,您将需要用户的收藏夹数组。如果该成分是用户的最爱,您将有条件地显示表情符号。
打开SaladItem.js
:
- nano src/components/SaladItem/SaladItem.js
导入useContext
和UserContext
,然后调用useContext
用UserContext
。之后,favorites
使用以下includes
方法检查成分是否在数组中:
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { createUseStyles } from 'react-jss';
import UserContext from '../User/User';
const useStyles = createUseStyles({
...
});
export default function SaladItem({ image, name }) {
const classes = useStyles();
const user = useContext(UserContext);
const favorite = user.favorites.includes(name);
return(
<div className={classes.wrapper}>
<h3>
{name}
</h3>
<span className={classes.favorite} aria-label={favorite ? 'Favorite' : 'Not Favorite'}>
{favorite ? '😋' : ''}
</span>
<button className={classes.add}>
<span className={classes.image} role="img" aria-label={name}>{image}</span>
</button>
</div>
)
}
SaladItem.propTypes = {
image: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}
保存并关闭文件。当你这样做时,浏览器将刷新,你会看到只有最喜欢的项目有表情符号:
与 不同Navigation
,上下文走得更远。组件树看起来像这样:
| User.Provider
| SaladMaker
| SaladBuilder
| SaladItem
信息跳过了两个没有任何道具的中间组件。如果您必须在整个树中将数据作为道具传递,这将是大量工作,而且您可能会冒着让未来的开发人员重构代码而忘记传递道具的风险。有了上下文,您就可以确信代码会随着应用程序的增长和演变而工作。
在此步骤中,您创建了一个上下文并使用Provider
来设置组件树中的数据。您还使用useContext
Hook访问了上下文并跨多个组件使用了上下文。这些数据是静态的,因此在初始设置后永远不会更改,但有时您需要共享数据并跨多个组件修改数据。在下一步中,您将使用上下文更新嵌套数据。
第 3 步 – 从嵌套组件更新数据
在这一步中,您将使用上下文和useReducer
Hook 来创建嵌套组件可以使用和更新的动态数据。您将更新SaladItem
组件以设置SaladSummary
将使用和显示的数据。您还将在根组件之外设置上下文提供程序。在此步骤结束时,您将拥有一个可以跨多个组件使用和更新数据的应用程序,并且您将能够在应用程序的不同级别添加多个上下文提供程序。
此时,您的应用程序正在跨多个组件显示用户数据,但缺少任何用户交互。在上一步中,您使用上下文来共享单条数据,但您也可以共享一组数据,包括函数。这意味着您可以共享数据,也可以共享更新数据的功能。
在您的应用程序中,每个人都SaladItem
需要更新一个共享列表。然后您的SaladSummary
组件将显示用户选择的项目并将其添加到列表中。问题是这些组件不是直接后代,所以你不能将数据和更新函数作为道具传递。但它们确实共享一个共同的父级:SaladMaker
.
上下文与其他状态管理解决方案(例如 Redux)之间的最大区别之一是上下文并非旨在成为中央存储。您可以在整个应用程序中多次使用它,并在根级别或组件树的深处启动它。换句话说,您可以在整个应用程序中传播您的上下文,创建集中的数据集合而不必担心冲突。
为了保持上下文聚焦,Providers
尽可能创建包装最近的共享父级。在这种情况下,这意味着App
您将在SaladMaker
组件中添加上下文,而不是在 中添加另一个上下文。
打开SaladMaker
:
- nano src/components/SaladMaker/SaladMaker.js
然后创建并导出一个名为 的新上下文SaladContext
:
import React, { createContext } from 'react';
import { createUseStyles } from 'react-jss';
import SaladBuilder from '../SaladBuilder/SaladBuilder';
import SaladSummary from '../SaladSummary/SaladSummary';
const useStyles = createUseStyles({
wrapper: {
textAlign: 'center',
}
});
export const SaladContext = createContext();
export default function SaladMaker() {
const classes = useStyles();
return(
<>
<h1 className={classes.wrapper}>
<span role="img" aria-label="salad">🥗 </span>
Build Your Custom Salad!
<span role="img" aria-label="salad"> 🥗</span>
</h1>
<SaladBuilder />
<SaladSummary />
</>
)
}
在上一步中,您为上下文创建了一个单独的组件,但在本例中,您将在使用它的同一文件中创建它。由于User
似乎与 没有直接关系App
,因此将它们分开可能更有意义。但是,由于 与组件SaladContext
紧密相关SaladMaker
,将它们放在一起将创建更具可读性的代码。
此外,您可以创建一个名为 的更通用的上下文OrderContext
,您可以在多个组件之间重用它。在这种情况下,您需要制作一个单独的组件。现在,让他们在一起。如果您决定转向另一种模式,您可以随时进行重构。
在添加之前Provider
,请考虑要共享的数据。您将需要一个项目数组和一个用于添加项目的函数。与其他集中式状态管理工具不同,上下文不处理数据更新。它只是保存数据以备后用。要更新数据,您需要使用其他状态管理工具,例如 Hooks。如果您正在为相同的组件收集数据,您可以使用useState
或useReducer
钩子。如果您不熟悉这些 Hook,请查看如何使用 React 组件上的 Hook 管理状态。
该useReducer
挂钩是一个不错的选择,因为你需要对每一个动作更新最近的状态。
创建一个reducer
向state
数组添加新项的函数,然后使用useReducer
Hook 创建一个salad
数组和一个setSalad
函数:
import React, { useReducer, createContext } from 'react';
import { createUseStyles } from 'react-jss';
import SaladBuilder from '../SaladBuilder/SaladBuilder';
import SaladSummary from '../SaladSummary/SaladSummary';
const useStyles = createUseStyles({
wrapper: {
textAlign: 'center',
}
});
export const SaladContext = createContext();
function reducer(state, item) {
return [...state, item]
}
export default function SaladMaker() {
const classes = useStyles();
const [salad, setSalad] = useReducer(reducer, []);
return(
<>
<h1 className={classes.wrapper}>
<span role="img" aria-label="salad">🥗 </span>
Build Your Custom Salad!
<span role="img" aria-label="salad"> 🥗</span>
</h1>
<SaladBuilder />
<SaladSummary />
</>
)
}
现在你有一个包含salad
你想要共享的数据的组件,一个被调用setSalad
来更新数据的函数,以及SaladContext
在同一个组件中共享数据的 。此时,您需要将它们组合在一起。
要合并,您需要创建一个Provider
. 问题是Provider
将单曲value
作为道具。既然你不能传递salad
和setSalad
个人,你需要将它们组合成一个对象,传递对象为value
:
import React, { useReducer, createContext } from 'react';
import { createUseStyles } from 'react-jss';
import SaladBuilder from '../SaladBuilder/SaladBuilder';
import SaladSummary from '../SaladSummary/SaladSummary';
const useStyles = createUseStyles({
wrapper: {
textAlign: 'center',
}
});
export const SaladContext = createContext();
function reducer(state, item) {
return [...state, item]
}
export default function SaladMaker() {
const classes = useStyles();
const [salad, setSalad] = useReducer(reducer, []);
return(
<SaladContext.Provider value={{ salad, setSalad }}>
<h1 className={classes.wrapper}>
<span role="img" aria-label="salad">🥗 </span>
Build Your Custom Salad!
<span role="img" aria-label="salad"> 🥗</span>
</h1>
<SaladBuilder />
<SaladSummary />
</SaladContext.Provider>
)
}
保存并关闭文件。与 一样Navigation
,当 与上下文SaladSummary
位于同一组件中时,似乎没有必要创建上下文。salad
作为道具传递是完全合理的,但您可能会在稍后重构它。在这里使用上下文可以将信息集中在一个地方。
接下来,进入SaladItem
组件并将setSalad
函数从上下文中拉出。
在文本编辑器中打开组件:
- nano src/components/SaladItem/SaladItem.js
在 内部SaladItem
,从 导入上下文SaladMaker
,然后setSalad
使用解构提取函数。向将调用该setSalad
函数的按钮添加一个单击事件。由于您希望用户能够多次添加一个项目,您还需要为每个项目创建一个唯一的 id,以便该map
函数能够分配一个唯一的id key
:
import React, { useReducer, useContext } from 'react';
import PropTypes from 'prop-types';
import { createUseStyles } from 'react-jss';
import UserContext from '../User/User';
import { SaladContext } from '../SaladMaker/SaladMaker';
const useStyles = createUseStyles({
...
});
const reducer = key => key + 1;
export default function SaladItem({ image, name }) {
const classes = useStyles();
const { setSalad } = useContext(SaladContext)
const user = useContext(UserContext);
const favorite = user.favorites.includes(name);
const [id, updateId] = useReducer(reducer, 0);
function update() {
setSalad({
name,
id: `${name}-${id}`
})
updateId();
};
return(
<div className={classes.wrapper}>
<h3>
{name}
</h3>
<span className={classes.favorite} aria-label={favorite ? 'Favorite' : 'Not Favorite'}>
{favorite ? '😋' : ''}
</span>
<button className={classes.add} onClick={update}>
<span className={classes.image} role="img" aria-label={name}>{image}</span>
</button>
</div>
)
}
...
要制作唯一 id,您将使用useReducer
Hook 在每次点击时增加一个值。对于第一次点击,id 将是0
; 第二个将是1
,依此类推。你永远不会向用户显示这个值;这将在稍后为映射函数创建一个唯一值。
创建唯一 id 后,您创建了一个函数,调用它update
来增加 id 并调用setSalad
. 最后,您将函数附加到带有onClick
道具的按钮上。
保存并关闭文件。最后一步是从SaladSummary
.
打开SaladSummary
:
- nano src/components/SaladSummary/SaladSummary.js
导入SaladContext
组件,然后salad
使用destructuring提取数据。用映射 over 的函数替换硬编码的列表项salad
,将对象转换为<li>
元素。请务必将id
用作key
:
import React, { useContext } from 'react';
import { createUseStyles } from 'react-jss';
import { SaladContext } from '../SaladMaker/SaladMaker';
const useStyles = createUseStyles({
...
});
export default function SaladSummary() {
const classes = useStyles();
const { salad } = useContext(SaladContext);
return(
<div className={classes.wrapper}>
<h2>Your Salad</h2>
<ul className={classes.list}>
{salad.map(({ name, id }) => (<li key={id}>{name}</li>))}
</ul>
</div>
)
}
保存并关闭文件。当您这样做时,您将能够单击项目并更新摘要:
Notice how the context gave you the ability to share and update data in different components. The context didn’t update the items itself, but it gave you a way to use the useReducer
Hook across multiple components. In addition, you also had the freedom to put the context lower in the tree. It may seem like it’s best to always keep the context at the root, but by keeping the context lower, you don’t have to worry about unused state sticking around in a central store. As soon as you unmount a component, the data disappears. That can be a problem if you ever want to save the data, but in that case, you just need to raise the context up to a higher parent.
Another advantage of using context lower in your application tree is that you can reuse a context without worrying about conflicts. Suppose you had a larger app that had a sandwich maker and a salad maker. You could create a generic context called OrderContext
and then you could use it at multiple points in your component without worrying about data or name conflicts. If you had a SaladMaker
and a SandwichMaker
, the tree would look something like this:
| App
| Salads
| OrderContext
| SaladMaker
| Sandwiches
| OrderContext
| SandwichMaker
Notice that OrderContext
is there twice. That’s fine, since the useContext
Hook will look for the nearest provider.
In this step you shared and updated data using context. You also placed the context outside the root element so it’s close to the components that need the information without cluttering a root component. Finally, you combined context with state management Hooks to create data that is dynamic and accessible across several components.
Conclusion
Context is a powerful and flexible tool that gives you the ability to store and use data across an application. It gives you the ability to handle distributed data with built-in tools that do not require any additional third party installation or configuration.
创建可重用上下文对于各种常见组件非常重要,例如需要跨元素访问数据的表单或需要选项卡和显示的公共上下文的选项卡视图。您可以在上下文中存储多种类型的信息,包括主题、表单数据、警报消息等。Context 使您可以自由地构建可以访问数据的组件,而无需担心如何通过中间组件传递数据或如何将数据存储在集中式存储中而不会使存储变得太大。
如果您想查看更多 React 教程,请查看我们的React 主题页面,或返回如何在 React.js 系列中编码页面。