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

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

介绍

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

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

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

先决条件

第 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边框和填充:

状态上下文教程/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.

保存并关闭文件。接下来, open 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用于将组件居中放置在页面上。元素rolearia-label属性span将有助于使用Accessible Rich Internet Applications (ARIA)进行可访问

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

  • nano src/components/App/App.js

SaladMakerNavigation组件之后导入和渲染

状态上下文教程/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添加了一个弱类型系统来提供一些强制措施,以确保不会传递错误的 prop 类型。

保存并关闭文件。现在,您需要渲染不同的项目。您将使用名为 的组件执行此操作,该组件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组件。确保在映射时为每个项目添加key此组件的样式增加了的显示flexFlexbox的布局,包裹物的组分,和中心它们。

保存并关闭文件。

最后,渲染组件,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

导入SaladSummary添加SaladBuilder

状态上下文教程/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 — 从根组件提供数据

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

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

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

  • mkdir src/components/User

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

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

  • nano src/components/User/User.js

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

状态上下文教程/src/components/User/User.js
import { createContext } from 'react';

const UserContext = createContext();
export default UserContext;

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

保存并关闭文件。

下一步是将上下文应用于一组元素。为此,您将使用一个名为 a 的组件ProviderProvider是设定数据,然后包装一些子组件的组件。任何包裹子组件将有从访问数据ProvideruseContext挂钩。

由于用户数据在整个项目中是不变的,因此尽可能将其放在组件树的高处。在此应用程序中,您将把它放在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对象将用户名保存为一个字符串和一最喜欢的成分。

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

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

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

打开Navigation.js:

  • nano src/components/Navigation/Navigation.js

在 内部NavigationuseContext从 React 和UserContext组件目录导入Hook 然后调用useContextusingUserContext作为参数。与 不同的是UserContext.Provider,您不需要UserContextJSX 中进行渲染Hook 将返回您在valueprop 中提供的数据将数据保存到一个名为 的新变量中user,该变量是一个包含name的对象favorites然后,您可以将硬编码名称替换为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>
  )
}

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

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

状态上下文教程/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来设置组件树中的数据。您还使用useContextHook访问了上下文并跨多个组件使用了上下文。这些数据是静态的,因此在初始设置后永远不会更改,但有时您需要共享数据并跨多个组件修改数据。在下一步中,您将使用上下文更新嵌套数据。

第 3 步 – 从嵌套组件更新数据

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

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

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

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

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

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

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

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

useReducer挂钩是一个不错的选择,因为你需要对每一个动作更新最近的状态。

创建一个reducerstate数组添加新项函数,然后使用useReducerHook 创建一个salad数组和一个setSalad函数:

状态上下文教程/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作为道具传递是完全合理的,但您可能会在稍后重构它。在这里使用上下文可以将信息集中在一个地方。

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

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

  • nano src/components/SaladItem/SaladItem.js

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

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

创建唯一 id 后,您创建了一个函数,调用它update来增加 id 并调用setSalad. 最后,您将函数附加到带有onClick道具的按钮上

保存并关闭文件。最后一步是从SaladSummary.

打开SaladSummary:

  • nano src/components/SaladSummary/SaladSummary.js

导入SaladContext组件,然后salad使用destructuring提取数据用映射 over 的函数替换硬编码的列表项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>
  )
}

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

添加沙拉项目

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 系列中编码页面

觉得文章有用?

点个广告表达一下你的爱意吧 !😁