如何使用上下文在React组件之间共享状态

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Creative Commons 来接受捐赠。

介绍

在本教程中,您将使用 React 上下文 在多个组件之间共享状态。 React 上下文是一个接口,用于与其他 组件 共享信息,而无需将数据显式传递为 props。 这意味着您可以在父组件和深度嵌套的子组件之间共享信息,或者将站点范围的数据存储在一个位置并在应用程序的任何位置访问它们。 您甚至可以通过与数据一起提供更新函数来更新嵌套组件中的数据。

React 上下文足够灵活,可以用作项目的集中状态管理系统,或者您可以将其范围缩小到应用程序的较小部分。 通过上下文,您可以在应用程序之间共享数据,而无需任何额外的第三方工具并且只需少量配置。 这为 Redux 之类的工具提供了更轻量级的替代方案,它可以帮助处理更大的应用程序,但对于中型项目可能需要过多的设置。

在本教程中,您将使用上下文来构建一个应用程序,该应用程序使用跨不同组件的通用数据集。 为了说明这一点,您将创建一个网站,用户可以在其中制作定制沙拉。 该网站将使用上下文来存储客户信息、最喜欢的项目和定制沙拉。 然后,您将访问该数据并在整个应用程序中更新它,而无需通过 props 传递数据。 在本教程结束时,您将学习如何使用上下文在项目的不同级别存储数据,以及如何访问和更新嵌套组件中的数据。

先决条件

第 1 步 — 为您的申请奠定基础

在此步骤中,您将构建自定义沙拉生成器的一般结构。 您将创建组件以显示可能的配料、所选配料的列表和客户信息。 当您使用静态数据构建应用程序时,您会发现如何在各种组件中使用不同的信息片段,以及如何识别在上下文中有用的数据片段。

这是您将构建的应用程序的示例:

请注意您可能需要跨组件使用的信息。 例如,用户名(在此示例中为 Kwame)在导航区域中显示用户数据,但您可能还需要用户信息来识别最喜欢的项目或结帐页面。 应用程序中的任何组件都需要能够访问用户信息。 查看沙拉生成器本身,每种沙拉配料都需要能够更新屏幕底部的 Your Salad 列表,因此您需要从以下位置存储和更新该数据每个组件也可以访问。

首先对所有数据进行硬编码,以便您可以计算出应用程序的结构。 稍后,您将从下一步开始添加上下文。 随着应用程序开始增长,上下文提供了最大的价值,因此在这一步中,您将构建几个组件来展示上下文如何跨组件树工作。 对于较小的组件或库,您通常可以使用包装组件和较低级别的状态管理技术,例如 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 本身。 这将包含标题以及构建器和底部的 Your Salad 列表。 包含成分的部分将是一个单独的组件,称为 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 提供边框和填充:

状态上下文教程/src/components/Navigation/Navigation.js

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 右对齐。

保存并关闭文件。 接下来,打开App.js,也就是项目的根目录:

nano src/components/App/App.js

导入 Navigation 组件并通过添加突出显示的行将其呈现在空标签内:

状态上下文教程/src/components/App/App.js

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> 标签的组件,其标题为:

状态上下文教程/src/components/SaladMaker/SaladMaker.js

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 使组件在页面上居中。 span 元素的 rolearia-label 属性将有助于使用 Accessible Rich Internet Applications (ARIA) 的可访问性。

保存并关闭文件。 打开 App.js 渲染组件:

nano src/components/App/App.js

导入 SaladMaker 并在 Navigation 组件之后渲染:

状态上下文教程/src/components/App/App.js

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

状态上下文教程/src/components/SaladItem/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,
}

imagename 将是道具。 代码使用favorite变量和三元运算符有条件地判断favorite图标是否出现。 favorite 变量稍后将通过上下文作为用户配置文件的一部分来确定。 现在,将其设置为 true。 样式会将收藏夹图标放置在卡片的右上角,并移除按钮上的默认边框和背景。 wrapper 类将添加一个小边框并转换一些文本。 最后,PropTypes 添加了一个弱类型系统来提供一些强制措施,以确保不会传递错误的道具类型。

保存并关闭文件。 现在,您需要渲染不同的项目。 您将使用名为 SaladBuilder 的组件来执行此操作,该组件将包含一个项目列表,它将转换为一系列 SaladItem 组件:

打开SaladBuilder

nano src/components/SaladBuilder/SaladBuilder.js

如果这是一个生产应用程序,这些数据通常来自应用程序编程接口 (API)。 但现在,使用硬编码的成分列表:

状态上下文教程/src/components/SaladBuilder/SaladBuilder.js

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() 数组方法 映射列表中的每个项目,将 nameimage 作为道具传递给 SaladItem 组件. 请务必在映射 时为每个项目添加一个键 。 该组件的样式为 flexbox 布局 添加了 flex 的显示,将组件包裹起来,并将它们居中。

保存并关闭文件。

最后,在 SaladMaker 中渲染组件,使其出现在页面中。

打开SaladMaker

nano src/components/SaladMaker/SaladMaker.js

然后导入 SaladBuilder 并在标题后渲染:

状态上下文教程/src/components/SaladMaker/SaladMaker.js

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 使它们换行:

状态上下文教程/src/components/SaladSummary/SaladSummary.jss

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

导入并在 SaladBuilder 之后添加 SaladSummary

状态上下文教程/src/components/SaladMaker/SaladMaker.js

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 步 — 从根组件提供数据

在此步骤中,您将使用上下文将客户信息存储在组件的根目录中。 您将创建一个自定义上下文,然后使用一个名为 Provider 的特殊包装组件,它将信息存储在项目的根目录中。 然后,您将使用 useContext Hook 与嵌套组件中的提供程序连接,以便显示静态信息。 在此步骤结束时,您将能够提供集中的信息存储,并使用存储在许多不同组件中的上下文中的信息。

最基本的上下文是共享信息的接口。 许多应用程序都有一些需要在应用程序之间共享的通用信息,例如用户偏好、主题信息和站点范围的应用程序更改。 使用上下文,您可以在根级别存储该信息,然后在任何地方访问它。 由于您在父项中设置了信息,因此您知道它将始终可用并且始终是最新的。

要添加上下文,请创建一个名为 User 的新目录:

mkdir src/components/User

User 不会是一个传统的组件,因为您将把它用作一个组件和一个名为 useContext 的特殊 Hook 的数据。 目前,保持平面文件结构,但如果您使用大量上下文,则可能值得将它们移动到不同的目录结构。

接下来,在文本编辑器中打开 User.js

nano src/components/User/User.js

在文件中,从 React 中导入 createContext 函数,然后执行该函数并导出结果:

状态上下文教程/src/components/User/User.js

import { createContext } from 'react';

const UserContext = createContext();
export default UserContext;

通过执行该函数,您已经注册了上下文。 结果 UserContext 就是您将在组件中使用的结果。

保存并关闭文件。

下一步是将上下文应用于一组元素。 为此,您将使用一个名为 Provider 的组件。 Provider 是设置数据然后包装一些子组件的组件。 任何包装的子组件都可以通过 useContext 钩子访问来自 Provider 的数据。

由于用户数据将在整个项目中保持不变,因此请尽可能将其放在组件树的较高位置。 在此应用程序中,您将把它放在 App 组件的根级别:

打开App

nano src/components/App/App.js

添加以下突出显示的代码行以导入上下文并传递数据:

状态上下文教程/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对象,它以字符串的形式保存用户名和最喜欢的成分的 array

接下来,您导入了 UserContext,然后使用名为 UserContext.Provider 的组件包装了 NavigationSaladMaker。 请注意在这种情况下 UserContext 是如何充当标准 React 组件的。 该组件将采用一个名为 value 的道具。 该道具将是您要共享的数据,在本例中是 user 对象。

保存并关闭文件。 现在数据在整个应用程序中都可用。 但是,要使用数据,您需要再次导入和访问上下文。

现在您已经设置了上下文,您可以开始用动态值替换组件中的硬编码数据。 首先将 Navigation 中的硬编码名称替换为您在 UserContext.Provider 中设置的用户数据。

打开Navigation.js

nano src/components/Navigation/Navigation.js

Navigation 内部,从 React 导入 useContext Hook 并从组件目录导入 UserContext。 然后使用 UserContext 作为参数调用 useContext。 与 UserContext.Provider 不同,您不需要在 JSX 中渲染 UserContext。 Hook 将返回您在 value 属性中提供的数据。 将数据保存到一个名为 user 的新变量中,该变量是一个包含 namefavorites 的对象。 然后,您可以用 user.name 替换硬编码名称:

状态上下文教程/src/components/Navigation/Navigation.js

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>
  )
}

UserContextApp.js 中用作组件,但在这里您更多地将其用作数据。 但是,如果您愿意,它仍然可以充当组件。 您可以使用 UserContext 中的 Consumer 访问相同的数据。 您可以通过将 UserContext.Consumer 添加到 JSX 来检索数据,然后使用 函数作为子函数来访问数据

虽然可以使用 Consumer 组件,但使用 Hooks 通常可以更短且更易于阅读,同时仍提供相同的最新信息。 这就是本教程使用 Hooks 方法的原因。

保存并关闭文件。 当您这样做时,页面将刷新并且您将看到相同的名称。 但这次它动态更新了:

在这种情况下,数据不会跨越许多组件。 表示数据经过的路径的组件树如下所示:

| UserContext.Provider
  | Navigation

您可以将此用户名作为道具传递,在这种规模下,这可能是一种有效的策略。 但是随着应用程序的增长,Navigation 组件有可能会移动。 可能有一个名为 Header 的组件包装了 Navigation 组件和另一个组件,例如 TitleBar,或者您可能会创建一个 Template 组件和然后将 Navigation 嵌套在那里。 通过使用上下文,只要 Provider 在树上,您就不必重构 Navigation,从而使重构更容易。

下一个需要用户数据的组件是 SaladItem 组件。 在 SaladItem 组件中,您需要用户的收藏夹数组。 如果成分是用户的最爱,您将有条件地显示表情符号。

打开SaladItem.js

nano src/components/SaladItem/SaladItem.js

导入 useContextUserContext,然后用 UserContext 调用 useContext。 之后,使用 includes 方法检查成分是否在 favorites 数组中:

状态上下文教程/src/components/SaladItem/SaladItem.js

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 挂钩来创建嵌套组件可以使用和更新的动态数据。 您将更新 SaladItem 组件以设置 SaladSummary 将使用和显示的数据。 您还将在根组件之外设置上下文提供程序。 在这一步结束时,您将拥有一个可以跨多个组件使用和更新数据的应用程序,并且您将能够在应用程序的不同级别添加多个上下文提供程序。

此时,您的应用程序正在跨多个组件显示用户数据,但它缺少任何用户交互。 在上一步中,您使用上下文来共享单个数据,但您也可以共享数据集合,包括函数。 这意味着您可以共享数据,也可以共享更新数据的功能。

在您的应用程序中,每个 SaladItem 都需要更新一个共享列表。 然后您的 SaladSummary 组件将显示用户选择的项目并将其添加到列表中。 问题是这些组件不是直接后代,因此您不能将数据和更新函数作为道具传递。 但他们确实有一个共同的父母:SaladMaker

上下文和其他状态管理解决方案(例如 Redux)之间的一大区别是上下文并非旨在成为中央存储。 您可以在整个应用程序中多次使用它,并在根级别或组件树的深处启动它。 换句话说,您可以将上下文分布在整个应用程序中,创建有针对性的数据集合,而不必担心冲突。

为了保持上下文关注,创建 Providers 尽可能包装最近的共享父级。 在这种情况下,这意味着您将在 SaladMaker 组件中添加上下文,而不是在 App 中添加另一个上下文。

打开SaladMaker

nano src/components/SaladMaker/SaladMaker.js

然后创建并导出一个名为 SaladContext 的新上下文:

状态上下文教程/src/components/SaladMaker/SaladMaker.js

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 没有直接关系,因此将它们分开可能更有意义。 但是,由于 SaladContextSaladMaker 组件紧密相关,因此将它们放在一起将创建更具可读性的代码。

此外,您可以创建一个更通用的上下文,称为 OrderContext,您可以在多个组件中重复使用它。 在这种情况下,您需要制作一个单独的组件。 现在,让他们在一起。 如果您决定转向另一种模式,您可以随时重构。

在添加 Provider 之前,请考虑您要共享的数据。 您将需要一个项目数组和一个用于添加项目的函数。 与其他集中式状态管理工具不同,上下文不处理数据更新。 它只是保存数据以供以后使用。 要更新数据,您需要使用其他状态管理工具,例如 Hooks。 如果您正在收集同一组件的数据,您将使用 useStateuseReducer Hooks。 如果您不熟悉这些 Hook,请查看 如何使用 React 组件上的 Hook 管理状态

useReducer Hook 非常适合,因为您需要更新每个操作的最新状态。

创建一个 reducer 函数,将新项目添加到 state 数组,然后使用 useReducer Hook 创建一个 salad 数组和一个 [ X137X]功能:

状态上下文教程/src/components/SaladMaker/SaladMaker.js

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 作为道具。 由于您不能单独传递 saladsetSalad,因此您需要将它们组合成一个对象并将对象作为 value 传递:

状态上下文教程/src/components/SaladMaker/SaladMaker.js

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 作为 prop 传递是完全合理的,但您可能会在以后重构它。 在这里使用上下文将信息保存在一个地方。

接下来,进入 SaladItem 组件并将 setSalad 函数拉出上下文。

在文本编辑器中打开组件:

nano src/components/SaladItem/SaladItem.js

SaladItem 内部,从 SaladMaker 导入上下文,然后使用解构提取 setSalad 函数。 向将调用 setSalad 函数的按钮添加点击事件。 由于您希望用户能够多次添加一个项目,您还需要为每个项目创建一个唯一的 id,以便 map 函数能够分配一个唯一的 [X184X ]:

状态上下文教程/src/components/SaladItem/SaladItem.js

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 钩子在每次点击时增加一个值。 对于第一次点击,id 将为 0; 第二个将是 1,依此类推。 你永远不会向用户显示这个值; 这只会在稍后为映射函数创建一个唯一值。

创建唯一 id 后,您创建了一个名为 update 的函数来增加 id 并调用 setSalad。 最后,您使用 onClick 属性将该功能附加到按钮上。

保存并关闭文件。 最后一步是从 SaladSummary 的上下文中拉取动态数据。

打开SaladSummary

nano src/components/SaladSummary/SaladSummary.js

导入SaladContext组件,然后使用解构拉出salad数据。 用映射 salad 的函数替换硬编码的列表项,将对象转换为 <li> 元素。 请务必将 id 用作 key

状态上下文教程/src/components/SaladSummary/SaladSummary.js

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>
  )
}

保存并关闭文件。 当您这样做时,您将能够单击项目并更新摘要:

请注意上下文如何使您能够共享和更新不同组件中的数据。 上下文不会更新项目本身,但它为您提供了一种跨多个组件使用 useReducer Hook 的方法。 此外,您还可以自由地将上下文放在树中的较低位置。 似乎最好始终将上下文保持在根目录,但通过保持较低的上下文,您不必担心未使用的状态会留在中央存储中。 一旦卸载组件,数据就会消失。 如果您想保存数据,这可能是个问题,但在这种情况下,您只需要将上下文提升到更高的父级。

在应用程序树中使用较低上下文的另一个优点是您可以重用上下文而不必担心冲突。 假设你有一个更大的应用程序,它有一个三明治机和一个沙拉机。 您可以创建一个名为 OrderContext 的通用上下文,然后您可以在组件中的多个点使用它,而不必担心数据或名称冲突。 如果你有一个 SaladMaker 和一个 SandwichMaker,树看起来像这样:

| App
  | Salads
    | OrderContext
      | SaladMaker
  | Sandwiches
    | OrderContext
      | SandwichMaker

请注意,OrderContext 出现了两次。 没关系,因为 useContext Hook 会寻找最近的提供者。

在此步骤中,您使用上下文共享和更新数据。 您还将上下文放置在根元素之外,以便它靠近需要信息的组件,而不会弄乱根组件。 最后,您将上下文与状态管理 Hook 相结合,以创建可跨多个组件访问的动态数据。

结论

Context 是一个强大而灵活的工具,它使您能够跨应用程序存储和使用数据。 它使您能够使用不需要任何额外的第三方安装或配置的内置工具来处理分布式数据。

在各种通用组件中创建可重用上下文非常重要,例如需要跨元素访问数据的表单或需要选项卡和显示的通用上下文的选项卡视图。 您可以在上下文中存储多种类型的信息,包括主题、表单数据、警报消息等。 上下文使您可以自由地构建可以访问数据的组件,而不必担心如何通过中间组件传递数据或如何将数据存储在集中存储中而不会使存储变得太大。

如果您想查看更多 React 教程,请查看我们的 React 主题页面,或返回 如何在 React.js 系列页面中编码