React-和库教程-二-

119 阅读35分钟

React 和库教程(二)

原文:React and Libraries

协议:CC BY-NC-SA 4.0

四、React 路由和材质——用户界面

在这一章中,你将学习如何集成两个有用的库来帮助你加速开发工作,并使你的代码更具可读性、可测试性和可维护性。它们是 React 路由和材质界面。

在本书中,我们将创建一个功能完整的应用,测试它的一部分,甚至将其发布到生产中。该应用将是一个销售产品的网站,有一个登录的会员区和许多应用中常见的其他元素。

在本章中,我们将通过做两个练习来开始这个应用项目。具体来说,我们将创建一个包含顶部菜单的可用项目,然后我们将添加下拉菜单和一个抽屉。

图 4-1 显示了完成本章后的最终结果。在接下来的章节中,我们将继续添加更多的子组件、样式表和其他库。在了解 React 的所有库和元素的同时,我们将继续构建我们的应用。

img/503823_1_En_4_Fig1_HTML.jpg

图 4-1

第四章的最终 app 结果

集成 React 路由

在本章的这一节,我们将集成一个大多数应用都需要的共同特性:路由。我们将把 React 路由的最新版本 v5.2.0 集成到 React TypeScript 项目中。

我们为什么需要路由?

我们为什么需要路由呢?React 不就是一个单页范例吗?在 React 的单页面应用(SPA)范式中,大多数应用都需要多个视图。即使您的应用很简单,没有很多组件,如果它只是一个不需要更改的单个网页,或者如果它可以内置到一个单一的主要组件中,在未来可能会发生变化,所以最好在项目的早期阶段将路由代码集成到应用中。

在单个组件上构建应用并不理想,因为代码和复杂性会增加,维护和测试会成为开发人员的噩梦。

如果你还记得第一章中的内容,我们在 React 中看到了 DOM 操作是如何发生的。通过 DOM 上的getElementByIdremoveChild等方法动态改变网页的内容。

我们在路由过程中真正做的是在 React 中使用一个单页应用,并使用 React 虚拟 DOM 在浏览器中动态切换不同的树对象。与传统的 HTML 范式相比,这种变化发生得更快,因为实际的 DOM 上只更新变化。

在这个“协调”过程中,React 会计算出哪些对象在不同的过程中发生了变化。然后 React 只更新“真正的”HTML DOM 中需要更改的对象。这加快了进程。

在本节中,我将向您展示如何使用最新版本的 React Router with TypeScript 并实现路由。

Note

React 是基于 SPA 的概念构建的,但是大多数应用实际上都是使用多个视图构建的。

要实现路由,您可以从一些流行的选项中进行选择。以下是最流行的路由库:

  • React 路由

  • 路由 5

  • 冗余优先路由

  • 到达路由

到达路由(也称为@reach/route)适用于数量较少的路由,并且应该可以在 React 路由和到达路由之间轻松地来回迁移。实际上,Reach 路由和 React 路由是由同一个团队打造的。React 路由被认为是必知的,是 GitHub 上最受欢迎的项目,有超过 61,000 颗星。

请注意,React Router 在版本 v5.x.

中发生了巨大的变化。“随着钩子的引入,React 从根本上改变了我们编写状态和行为的方式,我们希望利用它。”

—Ryan Florence,React Training 联合创始人/首席执行官

React Router 的好处包括性能和将 API 暴露给 React 挂钩,如locationnavigate挂钩。

您可以在 https://reacttraining.com/blog/reach-react-router-future/ 了解更多关于 React 路由的信息。你也可以在 https://reactrouter.com/web/api/ 查看 React API。

如何将 React 路由集成到 ReactJS 项目中?

我们将把这个过程分成两步。

  • 第一步:搭建,在这里我们创建菜单和页面

  • 步骤 2 :显示视图,在这里我们创建路由逻辑和链接

第一步:脚手架

我们将把我们的应用分成页眉、正文和页脚。见图 4-2 。

img/503823_1_En_4_Fig2_HTML.jpg

图 4-2

将我们的应用分成页眉、页脚和正文

我们还将包括一个抽屉菜单,将打开一个菜单图标。我们开始吧。

使用must-have-libraries创建一个新的 CRA 项目,并将其命名为练习-4-1。

$ yarn create react-app exercise-4-1 --template must-have-libraries

接下来,我们可以将目录更改为新项目并启动项目。

$ cd exercise-4-1/
$ yarn start

您可以在这里找到并下载本练习的完整代码:

https://github.com/Apress/react-and-libraries/exercise-4-1

首先,我们将修改src/App.tsx来显示一个div,它将输出“应用页面”而不是默认的 CRA 欢迎页面。稍后,当我们设置路由时,我们将在路由中使用该页面。新的App.tsx页面将如下所示:

// src/App.tsx

import React from 'react'
import './App.scss'

function App() {
  return (
    <div className="App">
      <div>App page</div>
    </div>
  )
}

export default App

由于index.tsx组件已经包含了App子组件,如果您运行yarn start,,您应该会看到我们所做的更改。见图 4-3 。

img/503823_1_En_4_Fig3_HTML.jpg

图 4-3

将 App.tsx 更改为输出“应用页面”后我们的应用

我们将使用 CRA MHL 附带的generate-react-cli库来创建我们的页面和布局元素。我们可以使用我们设置的定制模板用generate-react-cli创建HeaderFooter组件。在项目的根文件夹中运行以下命令:

$ npx generate-react-cli component Footer --type=layout

输出将为您提供为您创建的组件。它生成了 SCSS、Jest 测试文件和组件文件。例如,这里有一个Footer组件:

Stylesheet "Footer.scss" was created successfully at src/layout/Footer/Footer.scss
Test "Footer.test.tsx" was created successfully at src/layout/Footer/Footer.test.tsx
Component "Footer.tsx" was created successfully at src/layout/Footer/Footer.tsx

如果您打开src/layout文件夹,您将看到为我们创建的文件。见图 4-4 。

img/503823_1_En_4_Fig4_HTML.jpg

图 4-4

页脚组件文件夹和文件

该项目为我们建立了初始的测试 Jet 文件(在本书的后面你会学到更多关于测试的知识)。我们在第二章中讨论过。现在我们实际上使用 Lint 来运行脚本命令。然后,我们将格式化并测试它们,以确保我们的项目通过某些编码标准。

  • ESLint :我们会做代码分析,标记编程错误、bug、风格错误和任何其他可疑的构造。

  • Jest 测试:我们将确保我们的测试运行并通过。

  • 格式:我们将确保我们的代码使用我们设定的最佳实践进行格式化(在我们的例子中,我们设定了 Airbnb 风格)。

为此,运行这些命令,它们是我们在package.json中设置的运行脚本:

$ yarn format
$ yarn lint
$ yarn test

Tip

如果您得到任何 Lint 错误和警告,您可以运行$ yarn lint --fix来自动修复它们,并根据需要调整.eslintrc文件。

你可以把你的结果和我的比较一下,如图 4-5 所示。

img/503823_1_En_4_Fig5_HTML.jpg

图 4-5

格式、lint 和测试结果

Note

在本书的下一章中,我们将更详细地介绍测试,以及如何设置自动化开发和部署流程以及优化您的应用。在这里,我想至少向您展示这些任务,我强烈建议您在每次编码时执行,以确保您的代码质量。

类似地,要创建我们的Header布局组件,运行以下命令来生成组件文件:

$ npx generate-react-cli component Header --type=layout

如果打开模板文件,可以看到它被设置为React.PureComponent。将HooterFooter组件添加到渲染 JSX 输出中。

import React from 'react'
import './Header.scss'

export default class Header extends React.PureComponent<IHeaderProps, IHeaderState> {
  constructor(props: IHeaderProps) {
    super(props)
    this.state = {}
  }

  render() {
    return Header
  }
}

interface IHeaderProps {

  // TODO
}

interface IHeaderState {
  // TODO
}

在我们的应用中,我包含了一个销售数字内容的网站的不同页面,如联系人、书籍、课程、教练服务等。如果你愿意,你可以改变这些页面;这些只是建议。

$ npx generate-react-cli component HomePage --type=page
$ npx generate-react-cli component ContactPage --type=page
$ npx generate-react-cli component BooksPage --type=page
$ npx generate-react-cli component BuildSiteCoursePage --type=page
$ npx generate-react-cli component YouBuildMySitePage --type=page
$ npx generate-react-cli component CoachingHourlyPage --type=page
$ npx generate-react-cli component CoachingPackagePage --type=page
$ npx generate-react-cli component MembersPage --type=page
$ npx generate-react-cli component LoginPage --type=page
$ npx generate-react-cli component ArticlesPage --type=page
$ npx generate-react-cli component NotFoundPage --type=page

为了保持理智,运行 Lint 并测试它。如果您遵循了到目前为止的所有步骤,您应该会得到以下内容:

Test Suites: 15 passed, 15 total, and lint pass results

步骤 2:显示视图,在这里我们创建路由逻辑和链接

如果您打开 pages 组件,正如您在第一章中看到的,我们设置的模板文件示例包括一个带有 React Router 的 TypeScript 类组件和一个指向路径名的钩子。

我们导入 React 路由 DOM,然后使用一个钩子,我们可以访问当前浏览器位置的 URL 位置来显示名称。例如,如果 URL 是http://localhost:3000/homehome/,那么name变量就是home

import { RouteComponentProps } from 'react-router-dom'

this.state = {
  name: this.props.history.location.pathname.substring(
    1,
    this.props.history.location.pathname.length
  ).replace('/', '')
}

这个模板类中的react-route使用由react-route API 设置的钩子从react-route上的历史 API 中提取页面名称。

注意,在模板的接口类中,接口需要扩展RouteComponentProps才能使this.props.history工作。另一件需要做的事情是,父组件将这个子组件包装在一个<Router>标签中,这样钩子才能工作。

我们已经创建了页面,我们将用一个抽屉组件和菜单链接到这些页面。在本节中,我们将包括我们创建的所有页面,并设置带有页眉和页脚的页面布局。

为了完成所有这些,我们将创建一个子组件,我们可以将它包含在我们的主index.tsx组件中。

我们可以在index.tsx中编写所有这些代码,但是如果我们将这些代码分割成子组件,我们的代码将更易读、更易于测试和维护。

这就是为什么我们的模板项目中已经有了一个名为src/AppRouter.tsx的子组件。我们可以重构代码以包含 React 和 React Router 的导入以及所有组件页面。因此,我们将使用以下内容:

// src/AppRouter.tsx

import React from 'react'
import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom'
import App from './App'
import Home from './pages/HomePage/HomePage'
import Contact from './pages/ContactPage/ContactPage'
import Books from './pages/BooksPage/BooksPage'
import BuildSiteCourse from './pages/BuildSiteCoursePage/BuildSiteCoursePage'
import YouBuildMySite from './pages/YouBuildMySitePage/YouBuildMySitePage'
import CoachingHourly from './pages/CoachingHourlyPage/CoachingHourlyPage'
import CoachingPackage from './pages/CoachingPackagePage/CoachingPackagePage'
import Members from './pages/MembersPage/MembersPage'
import Login from './pages/LoginPage/LoginPage'
import Articles from './pages/ArticlesPage/ArticlesPage'
import NotFound from './pages/NotFoundPage/NotFoundPage'
import Footer from "./layout/Footer/Footer";
import Header from "./layout/Header/Header";

对于函数return部分,我们需要将每个组件包装在路由标签中,以便访问路由挂钩。HeaderFooter组件总是会出现在页面上,而内容会改变。

为了实现这一点,每个内容页面都被设置为具有确切路径的路由,如下所示:

function AppRouter() {
  return (
    <Router>
      <Header />
      <Switch>
        <Route exact path="/" component={App} />
<Route exact path="/Home" component={Home} />
<Route exact path="/contact" component={Contact} />
<Route exact path="/Books" component={Books} />
<Route exact path="/BuildSiteCourse" component={BuildSiteCourse} />
<Route exact path="/YouBuildMySite" component={YouBuildMySite} />
<Route exact path="/CoachingHourly" component={CoachingHourly} />
<Route exact path="/CoachingPackage" component={CoachingPackage} />
<Route exact path="/Members" component={Members} />
<Route exact path="/Login" component={Login} />
<Route exact path="/Articles" component={Articles} />
        <Route path="/404" component={NotFound} />
        <Redirect to="/404" />
      </Switch>
      <Footer />
    </Router>
  )
}
export default AppRouter

注意,404 NotFound组件有一个Redirect标签。我们将使用该组件,以防用户试图进入一个不存在的页面。

接下来,注意我们的项目已经在index.tsx中包含了AppRouter子组件,所以我们不需要做任何事情。我只是想给你指出来。

ReactDOM.render(<AppRouter />, document.getElementById('root'))

在这种状态下,最终结果应该如图 4-6 所示。

img/503823_1_En_4_Fig6_HTML.jpg

图 4-6

决赛成绩

此时,我们没有在页面间导航的菜单;但是,如果您将浏览器地址栏的 URL 更改为我们在路由中设置的页面之一,例如,如果我们导航到http://localhost:300/Home,我们将看到图 4-7 中的屏幕。

img/503823_1_En_4_Fig7_HTML.jpg

图 4-7

主页的最终结果

在本练习中,您学习了 React 路由。我们将我们的应用分为页眉、页脚和内容部分。我们生成了子组件,然后我们创建了一个路由App子组件来创建结构并能够使用 React 路由 API。

在本章的下一节,我们将学习 Material-UI CSS 框架,这样我们可以加快开发速度,创建菜单和抽屉,然后我们可以链接到我们创建的子组件页面。

集成材质-UI CSS 框架

到目前为止,您已经了解了 React 和 DOM 以及幕后发生的事情。您甚至学习了如何创建简单和复杂的组件,以及如何使用 React Router。

现在我们需要链接我们的页面。我们可以开始构建自己的定制组件;然而,为了加快开发速度,通常的做法是使用 CSS 框架。

在本章的这一节,我们将构建大多数网站都需要的通用元素。

为了创建一个带抽屉的顶层菜单,我们将从 Material-UI CSS 框架中获得帮助。

这个练习的完整代码可以从这里下载:

https://github.com/Apress/react-and-libraries/exercise-4-2

为什么我们需要一个 CSS 框架?

CSS 框架(或 CSS 库)为您的开发带来了更加标准化的实践。使用 CSS 框架,我们可以加快我们的开发工作,而不是仅仅使用普通的旧 CSS(或其他样式表),因为它允许我们使用预定义的元素。是的,我们可以从头开始创建所有这些定制组件,设计它们的样式,并在所有设备甚至遗留浏览器上测试它们,但是大多数时候这是不值得的。我们在这里不是要重新发明轮子。相反,我们可以只使用预定义的元素。

此外,请记住,您仍然可以在现有的项目中使用样式表,所以我们并没有放弃样式表或其他样式选项。框架只是提供了补充。

现在,当谈到 CSS 框架时,有一些流行的选项可供选择。React 项目中使用的主要工具(基于 GitHub stars)如下:

  • 自举:143000 颗恒星

  • Material-UI : 6 万颗星星

  • Bulma : 4 万颗星星

  • 语义 UI:483000 颗星

还有其他很棒的框架,比如 Tailwind、野餐 CSS、PaperCSS 以及其他许多适合解决不同挑战的框架。

虽然 Bootstrap 是最流行的框架,但我相信 Material-UI 是更好的选择。请记住,Bootstrap 可以与 Material-UI 一起使用,所以我们在这里不仅限于使用一个框架。

Material-UI 框架( https://material-ui.com/ )基于脸书的 React 框架,与 React 很好的集成。由脸书团队构建是一件大事,因为我们希望确保当我们将 React 项目升级到未来的 React 版本时,我们的代码不会中断。Material-UI 的 GitHub 页面在这里:

https://github.com/mui-org/material-ui

Note

Material-UI 项目包含根据 Material-UI 指南制作并遵循 Material-UI 设计原则的组件。

我们如何将 Material-UI 集成到我们的 ReactJS 项目中?

我们将添加的功能将是一个抽屉菜单,你可以打开链接,以及一个链接的标题顶部,将折叠为小屏幕尺寸。

是的,我们可以使用我们在前一个练习中创建的相同的Header组件;然而,这段代码会很复杂,很难测试和维护。

因此,最好将Header组件分解成子组件。我将Header组件分解成四个子组件。请看图 4-8 。

img/503823_1_En_4_Fig8_HTML.jpg

图 4-8

标题组件线框

你可以在这里看到这个野外遗址: https://elielrom.com

我们开始吧

在第一章的 Material-UI 所需的库方面,我们已经在 CRA 的香草风味上安装了这些库,所以我们已经准备好不用安装任何库就开始了。

让我们回顾一下我们安装了什么。我们在第一章安装了材质-UI 核心、图标和样式组件,它已经是我们起始项目的一部分了。如果你想回顾图书馆,请随意访问那一章。

我们使用styled-components的原因是它允许我们在 JavaScript 类中编写实际的 CSS。关于styled-components的更多细节,请访问图书馆的官方网站:

https://styled-components.com/

在接下来的章节中,我们将使用 CSS 和预处理器库,比如scss,所以我们不会在这里详细讨论样式和预处理器。然而,请注意,我们在应用中使用了文件扩展名.scss而不是.css,我们的项目被设置为能够处理scss文件类型。

标题组件

HeaderTheme组件将包装整个Header组件。选择架构的原因是我们可以让用户选择某些偏好,如应用的主题。我们还可以使用这个包装器来判断用户是否登录,以及用户正在使用什么类型的设备,然后通过将这些信息传递给子组件来相应地调整应用。

这种设计是理想的,因为我们不希望每个子组件都知道这些事情,让父组件传递这些信息可以让我们轻松地重构代码。

让我们回顾一下代码,如下所示:

// src/layout/Header/HeaderTheme.tsx

import React, { FunctionComponent } from 'react'
import AppBar from '@material-ui/core/AppBar/AppBar'
import { useMediaQuery } from '@material-ui/core'
import HeaderComponent from './Header'

function appBarBackgroundStyle() {
  return {
    background: '000000',
  }
}

export const HeaderTheme: FunctionComponent = () => {
  const smallBreakPoint = useMediaQuery('(min-width: 0px) and (max-width: 1100px)')
  return (
    <AppBar position="fixed" style={appBarBackgroundStyle()}>
      <HeaderComponent smallBreakPoint={smallBreakPoint} />
    </AppBar>
  )
}

请注意,我使用来自@material-ui/coreuseMediaQuery来判断我们是否需要为小屏幕实现任何逻辑。断点(smallBreakPoint)被设置为 1100 像素的分辨率,但是这可以被调整为不同的值。

然后我们使用 Material-UI 中的AppBar组件( https://material-ui.com/components/app-bar/ )将我们的组件包装在一个整洁的栏中,在里面我们有我们的HeaderComponent,在那里我们传递作为属性的smallBreakPointAppBar非常适合显示与当前屏幕相关的信息和动作。

我们使用一个函数将我们的AppBar颜色设置为白色,我们可以将主题调整为不同的颜色,甚至可以从父组件中调整。

标题子组件

Header子组件将包装这两个子组件:

  • HeaderTopNav

  • HeaderDrawer

// src/layout/Header/Header.tsx
import HeaderDrawer from './HeaderDrawer'
import HeaderTopNav from './HeaderTopNav'

在代码层面上,import语句包括我们将使用的组件以及 Material-UI 组件。我们将使用ToolbarBoxButton组件。

你可以在 https://material-ui.com/ 了解更多关于这些材质界面组件的信息。

import Toolbar from '@material-ui/core/Toolbar'
import Box from '@material-ui/core/Box'
import Button from '@material-ui/core/Button'

导入我们将使用的样式文件和字体。

import './Header.scss'

有几种方法可以导入字体;一个简单的方法是使用纱线。

$ yarn add fontsource-open-sans
import 'fontsource-open-sans'

我们将进口 React 和风格 SCSS。

import React from 'react'
import './Header.scss'

为了导航不同的页面,我们可以使用路由链接组件。React Router 支持LinkNavLink,因此我们可以将组件链接到不同的页面。

// router
import { Link } from 'react-router-dom'

在类定义中,我们不需要任何钩子,所以最好使用PureComponent,因为它能给我们最好的性能。

此时我们不需要存储任何状态,对于属性,我们需要设置从父组件传递过来的smallBreakPoint

interface IHeaderProps {
  smallBreakPoint: boolean
}

interface IHeaderState {
  // TODO
}

export default class Header extends React.PureComponent<IHeaderProps, IHeaderState> {
  constructor(props: IHeaderProps) {
    super(props)
    this.state = {}
  }

在 render 方法中,我们可以定义我们的组件。我们将把所有东西都包装在一个Toolbar组件中,并用position设置一个 box 组件,因为我们希望用我们设置的媒体查询折叠导航器,并且我们希望确保顶部的元素留在屏幕上。

看一看我们将使用的材质 UI 元素:

<Toolbar>
     <Box>
             Logo
     </Box>
     <Box>
            <Nav />
     </Box>
      <Box>
            <Drawer />
     </Box>
</ToolBar>

请注意,我使用的是材质框( https://material-ui.com/components/box/ ),它适合我们的需要,因为我们可以设置层。flexGrow组件将我们的导航项目保持在同一级别( https://material-ui.com/system/flexbox/#flex-grow )。

让我们实现它,如下所示:

render() {
  return (
    <Toolbar>
      <div style={{ width: '100%' }}>

Material-UI 中的按钮已经包含了我们可以用来导航到页面的属性。例如,当单击我们的徽标时,我们希望导航回主页/

          <Box display="flex" p={1}>
            <Box p={1} flexGrow={1}>
              <Button component={Link} to="/">
                ELI ELAD ELROM
              </Button>
            </Box>

接下来,我们可以使用内嵌逻辑,或者显示HeaderTopNav或者通过显示Nav组件来折叠它。

            <Box p={1}>
              {this.props.smallBreakPoint ? (
                <nav />
              ) : (
                <HeaderTopNav />
              )}
            </Box>
            <Box p={1}>
              <div
                style={{
                  position: 'absolute',
                  right: '0.5rem',
                }}
              >

抽屉密码可能在这里。然而,这会使代码更难阅读,所以我将代码分解成另一个子组件,名为:HeaderDrawer

                <HeaderDrawer />
              </div>
            </Box>
          </Box>
        </div>
      </Toolbar>
    )
  }
}

HeaderTopNav 子组件

在屏幕足够大的情况下,HeaderTopNav子组件将显示页面的链接,或者在用户屏幕较小的情况下,仅显示抽屉图标以打开抽屉。我们将使用材质界面的ButtonMenuItem

为了链接页面,我们将同时使用LinkNavLink,以及我们在第一章中介绍的styled-components库,它允许我们在组件内部创建一个styles对象。

// src/layout/Header/HeaderTopNav.tsx

import React from 'react'
import Button from '@material-ui/core/Button'
import Menu from '@material-ui/core/Menu'
import { Link, NavLink } from 'react-router-dom'
import MenuItem from '@material-ui/core/MenuItem'
import styled from 'styled-components'

Material-UI 带有我们可以使用的预定义图标。因此,我们也将导入它们。

你可以在这里查看所有不同的材质界面图标: https://material-ui.com/components/material-icons/

我们将使用 GitHub 图标,以及图标按钮 API ( https://material-ui.com/api/icon-button/ )

import IconButton from '@material-ui/core/IconButton'
import GitHubIcon from '@material-ui/icons/GitHub'

接下来,我们将使用styled-components库为链接的悬停状态设置一些 CSS 样式。我使用 0.5 秒的过渡来改变悬停时的颜色为灰色。

const DetectHover = styled.div`
  transition-duration: 0.5s;
  :hover {
    color: grey;
    span {
      opacity: 1;
    }
  }
`

我们也会考虑媒体的询问。CSS 中的媒体查询允许我们为不同的屏幕尺寸放置特定的逻辑。在我们的例子中,如果屏幕太小,我们希望折叠导航链接,因为我们有许多页面,链接不适合小屏幕。这就是为什么我们将使用一个抽屉,它会打开同样的链接。我将导航的样式设置为block,在 400 像素以下的小屏幕上隐藏导航,在 1100 像素以下的大屏幕上显示。

对于Nav,我们不需要任何风格;我们刚刚设置了组件。

const Nav = styled.nav``

我们需要跟踪一些事情,我们可以将它们设置为应用的状态。

例如,我们需要组件锚点的位置,我们可以用它来对齐下拉菜单以及标志,以指示下拉菜单是否打开。

为此,让我们设置anchorElement,我们将使用它来跟踪锚的位置。

let anchorElement: HTMLButtonElement

对于标志,我们有三个标志,因为我们将设置三个下拉菜单,每个父菜单一个(Build My Site、Coaching 和 Resources 父菜单项)。

interface IHTNState {
  menuBuildItem1Flag: boolean
  menuBuildItem2Flag: boolean
  menuBuildItem3Flag: boolean
}

在这一点上,我们不需要props,但是我们还是会定义它,因为我们的代码可能会改变,并且将来很有可能需要props

interface IHTNavProps {
  // TODO
}

至于实际的组件,我们正在创建一个 React TypeScript 类组件,我们传递定义和设置应用初始状态的propsstate接口,将所有标志设置为 false,因为所有菜单都将关闭。

export default class HeaderTopNav extends React.PureComponent<IHTNavProps, IHTNState> {
  constructor(props: IHTNavProps) {
    super(props)
    this.state = {
      menuBuildItem1Flag: false,
      menuBuildItem2Flag: false,
      menuBuildItem3Flag: false,
    }
  }

接下来,我们需要一种机制来处理用户单击顶部链接并打开包含更多链接的下拉菜单的情况。我们可以用鼠标事件处理器和开关来实现。在switch案例中,我们将设置打开下拉菜单的标志,并设置锚点,使下拉菜单与正确的组件对齐。

我们给每个switch案例一个编号,这样我们就知道是谁调用了这个处理程序(点击了哪个下拉菜单)。

接下来,我们还需要三个处理者。

  • 当用户单击一个菜单项时,我们希望在选择一个菜单项后像预期的那样关闭切换菜单。

  • 当用户点击菜单关闭图标按钮时,我们要关闭所有的子菜单下拉菜单。

  • 我们希望有一个开关的抽屉图标来打开和关闭抽屉。

看一看:

  handleMenuOpen = (event: React.MouseEvent, item: string) => {
    anchorElement = (event as React.MouseEvent<HTMLButtonElement>).currentTarget
    switch (item) {
      case '1':
        this.setState((prevState) => {
          return {
            ...prevState,
            menuBuildItem1Flag: true,
          }
        })
        break
      case '2':
        this.setState((prevState) => {
          return {
            ...prevState,
            menuBuildItem2Flag: true,
          }
        })
        break
      case '3':
        this.setState((prevState) => {
          return {
            ...prevState,
            menuBuildItem3Flag: true,
          }
        })
        break
    }
  }

  handleMenuClose = () => {
    this.setState((prevState) => {
      return {
        ...prevState,
        menuBuildItem1Flag: false,
        menuBuildItem2Flag: false,
        menuBuildItem3Flag: false,
      }
    })
  }

  render() {
    return (
      <Nav>
        <Button onClick={(event) => this.handleMenuOpen(event, '1')}>Build My Website</Button>

        <Menu id="menu-appbar1" anchorEl={anchorElement} open={this.state.menuBuildItem1Flag} onClose={this.handleMenuClose}>

每个下拉菜单父项都有一些到其他页面的链接。我们可以通过创建一个数组来设置它们,然后将该数组映射到一个NavLink React 路由组件,该组件将导航到我们想要的链接。

带有映射的数组允许我们用所有的NavLink组件创建一个循环,而不是创建几个NavLink组件。这非常方便,尤其是当您有不确定数量的项目,或者您想要将数据连接到后端数据源时。

          {[
            { name: 'Build My Own Site', url: '/BuildSiteCourse' },
            { name: 'You Build My Site', url: '/YouBuildMySite' },
          ].map((itemObject, index) => (
            <NavLink exact to={itemObject.url} className="NavLinkItem" key={itemObject.name}>
              <MenuItem onClick={this.handleMenuClose}>{itemObject.name}</MenuItem>
            </NavLink>
          ))}
        </Menu>

每个导航项都映射到一个事件处理程序,以打开下拉菜单。我们为每个菜单项设置一个数字开关。

        <Button onClick={(event) => this.handleMenuOpen(event, '2')}>Coaching</Button>
        <Menu id="menu-appbar2" anchorEl={anchorElement} getContentAnchorEl={null} open={this.state.menuBuildItem2Flag} onClose={this.handleMenuClose}>
          {[
            { name: 'Hourly', url: '/CoachingHourly' },
            { name: 'Packages', url: '/CoachingPackage' },
          ].map((itemObject, index) => (
            <NavLink exact to={itemObject.url} className="NavLinkItem" key={itemObject.name}>
              <MenuItem onClick={this.handleMenuClose}>{itemObject.name}/MenuItem>
            </NavLink>
          ))}
        </Menu>

我们可以对每个父按钮重复相同的过程。

        <Button onClick={(event) => this.handleMenuOpen(event, '3')}>Resources</Button>
        <Menu id="menu-appbar2" anchorEl={anchorElement} getContentAnchorEl={null} open={this.state.menuBuildItem3Flag} onClose={this.handleMenuClose}>
          {[
            { name: 'Books', url: '/Books' },
            { name: 'Articles', url: '/Articles' },
          ].map((itemObject, index) => (
            <NavLink exact to={itemObject.url} className="NavLinkItem" key={itemObject.name}>
              <MenuItem onClick={this.handleMenuClose}>{itemObject.name}</MenuItem>
            </NavLink>
          ))}
        </Menu>

如果我们有一个没有孩子的导航项目,我们可以使用按钮并在组件属性中绑定Link。以此处显示的联系人链接为例:

        <Button component={Link} to="/Contact">
          Contact
        </Button>

我们也可以使用 Material-UI 中的IconButton并使用href。这在 JSX 的表现就像你在 HTML 中期望的那样,并且引用一个外部 URL。

Tip

我们使用Link而不使用href的原因是我们不希望页面被刷新,这将减慢在 SPA 上使用这种设置翻页的过程。

        <a href="https://github.com/EliEladElrom/react-tutorials" target="_blank" rel="noopener noreferrer">
          <IconButton>
            <DetectHover>
              <GitHubIcon fontSize="large" />
            </DetectHover>
          </IconButton>
        </a>
      </Nav>
    )
  }
}

React 事件处理程序

如果您来自 JavaScript 世界,您应该知道所使用的事件处理程序。React 使用 JSX 并有自己的事件系统。

这就是为什么我们不能使用我们习惯的来自 JS 的事件比如叫做MouseEventhandleMenuOpen方法。

这段代码是错误的,会产生 Lint 错误:

onClickHandler = (e: MouseEventHandler)

Tip

我们需要使用React.MouseEvent;否则,我们会得到一个错误,或者我们将不能访问方法。一般来说,大多数事件的映射名称与 HTML 页面中使用的 JavaScript 名称相同。

幸运的是,React 类型化为您提供了标准 DOM 中您可能熟悉的每个事件的适当等价物。

我们可以使用React.MouseEvent或者从 React 模块导入MouseEvent类型。

这里有一个例子:

const onClickHandler = (e: React.MouseEvent) => {
  e.preventDefault()
}<Button type="submit" onClick={onClickHandler}>

HeaderDrawer 子组件

HeaderDrawer子组件将为HeaderTopNav子组件提供一个菜单图标,加上包含相同导航链接的实际抽屉。

我们需要HeaderTopNavHeaderDrawer子组件的原因是,当用户的屏幕小于 1100 像素时,我们可以容纳所有的导航链接,用户应该仍然能够使用菜单项和使用抽屉在页面之间导航。这是移动设备上常见的用户界面(UI)设计。

在编码层面上,我们将导入将要使用的 Material-UI 组件,以及styled-componentsNavLink和 Material-UI 图标。

// src/layout/Header/HeaderDrawer.tsx

import Drawer from '@material-ui/core/Drawer'
import Divider from '@material-ui/core/Divider'
import List from '@material-ui/core/List'
import { NavLink } from 'react-router-dom'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import WebIcon from '@material-ui/icons/Web'
import WebAssetIcon from '@material-ui/icons/WebAsset'
import ListItemText from '@material-ui/core/ListItemText'
import SupervisorAccountIcon from '@material-ui/icons/SupervisorAccount'
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle'
import MenuBookIcon from '@material-ui/icons/MenuBook'
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks'
import MailIcon from '@material-ui/icons/Mail'
import IconButton from '@material-ui/core/IconButton'
import React from 'react'
import MenuIcon from '@material-ui/icons/Menu'
import styled from 'styled-components'

我们将使用styled-components来设置悬停状态的样式。

const DetectHover = styled.div`
  transition-duration: 0.5s;
  :hover {
    color: grey;
    span {
      opacity: 1;
    }
  }
`

propsstate的接口将包括toggleMenuFlag的状态,以指示抽屉是否打开。

interface IHDProps {
  // TODO
}

interface IHDState {
  toggleMenuFlag: boolean
}

默认状态是抽屉关闭,我们需要绑定将要创建的handleToggle方法,以便它在渲染阶段可用。

export default class HeaderDrawer extends React.PureComponent<IHDProps, IHDState> {
  constructor(props: IHDProps) {
    super(props)
    this.state = {
      toggleMenuFlag: false,
    }
    this.handleToggle = this.handleToggle.bind(this)
  }

一旦单击了列表项,我们就将子组件的状态设置为 false。

Note

...prevState保持之前的状态。我们在这里不需要它,因为我们在状态中只有一个变量,但是总是这样写代码是一个好习惯,以防我们添加另一个状态变量。

  handleListItemClick = () => {
    this.setState((prevState) => {
      return {
        ...prevState,
        toggleMenuFlag: false,
      }
    })
  }

在 toggle 上,我们只是切换状态,由于状态是绑定的,它会关闭或打开抽屉。

  handleToggle() {
    this.setState((prevState) => {
      const newState = !prevState.toggleMenuFlag
      return {
        ...prevState,
        toggleMenuFlag: newState,
      }
    })
  }

  render() {
    return (

至于抽屉,我们需要一个图标按钮,它将通过我们创建的handleToggle方法的事件处理程序来打开和关闭(切换)我们的抽屉。

点击事件上的切换图标被绑定到handleToggle,它将切换状态。

        <IconButton style={{ color: 'black' }} onClick={this.handleToggle}>
          <DetectHover>
            <MenuIcon fontSize="large" />
          </DetectHover>
        </IconButton>

对于抽屉本身,我们将创建一个链接列表。我们将使用List Material-UI 组件创建它们,并再次使用带有贴图的数组来创建几个NavLink React 路由组件并链接到这些页面。我只展示了第一个List,但是 GitHub 中的代码包含了所有这些。也是同样的过程。List标签也可以包装在另一个数组中,以避免复制和粘贴同一段代码。

        <Drawer anchor="left" open={this.state.toggleMenuFlag} onClose={this.handleToggle}>
          <Divider />

          <List>
            {[
              { name: 'Build My Site', url: '/BuildSiteCourse' },
              { name: 'We Build Site', url: '/YouBuildMySite' },
            ].map((itemObject, index) => (
              <NavLink to={itemObject.url} className="NavLinkItem" key={itemObject.url} activeClassName="NavLinkItem-selected">
                <ListItem button key={itemObject.name} onClick={this.handleListItemClick}>
                  <ListItemIcon>{index % 2 === 0 ? <WebIcon /> : <WebAssetIcon />}</ListItemIcon>
                  <ListItemText primary={itemObject.name} />
                </ListItem>
              </NavLink>
            ))}
          </List>
          <Divider />

    )
  }
}

Header.scss

最后,在我们的Header.scss样式文件中,我们没有使用任何特定的scss样式特性。这是普通的 CSS。我们为NavLinkItemNavLinkItem-selected设置了字体系列和样式。看一看:

.Header {
  font-family: 'Open Sans', sans-serif;
  font-weight: 700;
}

.NavLinkItem {
  color: black;
  max-width: 360px;
  text-decoration: none;
}

.NavLinkItem-selected nav {
  background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
}

footer . scss

至于Footer组件,我想从顶部对齐 500 像素,从左侧填充 20 像素。因为我们的页面是空的,所以现在这样就可以了。

.Footer {
  position: relative;
  padding-top: 500px;
  padding-left: 20px;
}

SCS 页面

为了让页面在菜单下对齐,我们可以放置一个普通的 CSS 样式,或者只设置每个页面的填充,这样它们就可以很好地对齐。

.ArticlesPage {
  padding-top: 120px;
  padding-left: 20px;
}

AppRouter.tsx

最后,我们需要将Header子组件切换到我们创建的HeaderTheme组件。

function AppRouter() {
  return (
    <Router>
      <HeaderTheme />
      ...
    </Router>
  )
}

图 4-1 已经显示了最终结果。如果我们切换我们的抽屉图标,我们可以看到抽屉随着我们的抽屉链接向左打开。见图 4-9 。

img/503823_1_En_4_Fig9_HTML.jpg

图 4-9

抽屉练习的最终结果

你可以从这里下载这个练习的完整代码:

https://github.com/Apress/react-and-libraries/exercise-4-2

摘要

在本章中,我们在学习 React 路由和 Material-UI 框架的同时完成了两个练习。我们还学习了更多关于创建真实组件的知识。

我们开始构建一个包括页眉、页脚和内容区域的应用,并创建了一个带有导航链接的顶部菜单、一个下拉菜单和一个抽屉。我们还通过媒体查询调整了内容,以改进我们在不同屏幕尺寸上的导航和内容。

在下一节中,我们将继续构建我们的应用,同时开始集成状态管理,这将允许我们在不破坏 React 架构的情况下管理我们的状态并在不同组件之间进行通信。

五、状态管理

在前一章中,我们借助 Material-UI 构建了一个带有菜单和抽屉的 header 组件。我们还使用 React 路由连接页面。我们学习了 React 组件以及如何使用props将信息从父组件传递到子组件。我们还学习了组件生命周期和组件状态。然而,如果我们想在大多数应用中实现其他公共元素,如存储用户信息、购物车或多个组件或子组件可能需要的任何其他基于数据的状态,我们需要其他库的帮助来构建高质量的软件。

在这一章中,我将向你介绍一个重要的库,它是你的 React 武库工具箱中的必备之物。这是一个状态管理库,它可以帮助确保您编写的防弹代码不会随着时间的推移而变得混乱,难以调试和测试,并导致重构和添加新功能的噩梦。

具体来说,您将了解由脸书引入的状态管理架构,称为 Flux,然后您将了解撰写本文时最流行的状态管理,称为 Redux。最后,您将逐步学会使用 Redux 工具包。在这个过程中,我们将在上一章开始构建的应用中实现 Redux 工具包。

当您完成本章时,您将已经创建了您可以在图 5-1 中看到的最终结果。

img/503823_1_En_5_Fig1_HTML.jpg

图 5-1

最终结果包括一个带有主题的页脚

如果你看我们的页脚,你会注意到它有特殊的力量。具体来说,用户将能够调整我们的应用的主题从亮到暗,或反之亦然。应用的配色方案会相应改变,包括菜单上的字体颜色。图 5-2 显示了更改为黑暗主题的结果。

img/503823_1_En_5_Fig2_HTML.jpg

图 5-2

最终结果:更改应用主题

状态管理架构

当用户点击“更改主题”按钮时(参见图 5-2 中的页脚),我们希望相应地更改我们的应用以及所有组件和子组件的外观。为此,我们可以将用户的数据存储在一个用户首选项对象中。

决定的主题颜色的用户偏好是数据,或者换句话说,是我们的应用的“状态”,我们希望数据以一个方向流向我们的组件和子组件,这意味着组件不需要发送回数据。他们只需要收到一条消息,说明数据已经更改。一旦我们的视图收到数据已经更改的消息,我们就在组件和子组件(我们的视图)中进行更改。

为什么我们需要一个状态管理架构?

这种改变本身听起来微不足道,而且易于实施和管理。那么,为什么我们需要一个状态管理库来完成这个任务呢?

通俗地说,状态管理帮助组织你的 app 的数据和用户交互,直到用户的会话结束。它还有助于确保您的代码不会因为添加了更多功能而变得混乱。它使测试变得更加容易,并确保代码不依赖于特定的开发,并且可以扩展。

Note

状态管理是一种在用户会话结束前维护应用状态的方法。

在这一点上,我们并不真的需要一个设计模式来帮助我们管理我们的数据移动,并且实现一个架构来控制我们的数据移动对于这样简单的功能来说可能被认为是大材小用。

然而,随着我们的代码或团队的增长,我们需要某种架构来帮助处理数据移动,以及强制执行最佳实践来帮助管理我们的代码,以便它不会随着每次更改而中断。

事实上,脸书遇到了这些挑战,并寻找解决这些问题的方法。

脸书解决方案:Flux

脸书团队在扩大规模和维护代码方面的问题导致他们首先尝试现有的东西。他们首先实现了模型-视图-控制器(MVC)模式;然而,他们发现随着越来越多的特性被添加进来,架构模式引起了一些问题。一部分代码很难维护,而且代码经常出错。

什么是 MVC,它解决什么?

在一个复杂的应用中,建议把关注点分开(想想把你的衣服分开来洗)。

  • 模型:模型就是 app 数据。

  • 视图:视图是前端的表示层。这是一个不应直接更新的实现;理想情况下,它应该通过反射设计模式进行更新。反射意味着一旦数据改变,视图也会“神奇地”改变

  • 控制器:这是绑定模型和视图的光晕。

脸书团队尝试使用 MVC,但他们遇到了可能导致循环的数据流问题,这反过来会导致应用崩溃,因为它会成为内存泄漏(嵌套更新的级联效应)。

脸书团队解决了这些问题,他们提出了一个名为 Flux 的架构,最近又提出了一个名为反冲的实验库(我们将在下一章讨论和实现)。

Note

Flux 是一个用于构建用户界面的应用架构。 https://facebook.github.io/flux/

“Flux 是脸书用来构建客户端 web 应用的应用架构。它通过利用单向数据流来补充 React 的可组合视图组件。这更多的是一个模式而不是一个正式的框架“

——https://facebook.github.io/flux/docs/in-depth-overview

Flux 是脸书团队使用的一种数据管理模式。焊剂可以分解成三种成分。

  • Dispatcher :中心 hub 保存向 Dispatcher 注册的回调,并将数据发送到商店。数据是通过动作发送的。

  • 存储:顾名思义,存储的是数据。换句话说,它们保存着应用的状态。

  • 视图(我们的组件):视图是前端实现。我们的组件应该旨在尽可能的纯净和无状态,并且一旦状态改变就自动更新(通过使用反射)。

数据将像这样在应用中流动:

行动➤调度员➤店➤查看

使用这种类型的架构可以让我们知道数据来自哪里,去往哪里。这使得任何开发人员都可以“跳进去”并开始编码,因为他们可以观察动作并快速解决问题,还可以轻松地帮助调试、测试和实现新功能。

例如,假设我们想要更改应用的状态。视图上的用户交互可以调用触发 dispatcher 回调的操作,这将更新应用在商店中的状态,最终这些更改将在视图组件中使用。见图 5-3

img/503823_1_En_5_Fig3_HTML.jpg

图 5-3

Flux 数据流(信用: https://facebook.github.io/flux/docs/in-depth-overview )

脸书提出的应用架构 Flux 旨在通过以下方式实现其目标:

  • 显式数据:使用显式数据,不使用派生数据(显式数据是未插入的原始数据)。

  • 分离关注点:从视图中分离数据,就像在 MVC 中一样。

  • 避免级联效应:防止嵌套更新(MVC 模式下可能发生的),这是 Flux 和 MVC 最大的区别。

观看此视频可以了解更多脸书 Flux 心态: https://youtu.be/nYkdrAPrdcw .

我使用过许多大大小小的基于 MVC 的应用,有些是构建在 MVC 基础上的复杂的企业级应用。我不得不有点不同意脸书团队。通过加强良好的习惯,基于 MVC 的应用可以无缝地工作。也就是说,在许多 MVC 框架实现中涉及到大量的样板代码,并且代码审查通常是必要的,以加强良好的习惯并保持关注点的分离。

脸书的 Flux 架构确实简化了分离关注点的过程,并且是状态管理的一种新的替代方式,同时保持了较少的样板代码和松散耦合的组件。

香草冰淇淋

有许多状态管理库可供选择。以下是一些例子:

事实上,状态管理非常重要,它可以成为选择一个框架而不是另一个框架的理由。请记住,React 不是一个框架,而是一个库,不像 Angular 或 Vue 等其他框架那样与 React 竞争,它没有开箱即用的关注点分离。

在撰写本文时,实现 Flux 的最流行的状态管理工具是 Redux。脸书团队还在 2020 年年中提出了自己的状态管理库,名为反冲( https://recoiljs.org/ )。它非常有前途,我将在下一章中介绍它。然而,在撰写本书时,它仍处于起步阶段,还没有经过实验阶段而变得成熟。

为什么 Redux

正如 Redux.js 组织所言,

Redux 是一个用于管理应用状态的开源 JavaScript 库。它通常与 React 或 Angular 等库一起用于构建用户界面。

vanilla Redux 版本是非个人化的,这意味着您可以在架构和实践方面随心所欲。然而,Redux 团队官方推荐的方法是使用一个名为 Redux 工具包 的独立附加包,其中包括一些固执己见的默认设置,以帮助更有效地使用 Redux。是 Redux 配合 React 使用的标准。

Note

Redux 的香草味不推荐,这一节只是帮助你了解 Redux。Redux 工具包 更易于使用,是大多数应用的推荐方法。

在继续介绍 Redux 工具包之前,我将首先向您展示 Redux。

首先学习 Redux 更容易,因为它基于相同的组件。您将很快获得 Redux 工具包。

怎么才能学会 Redux?

为了教你 Redux,我把这个过程分解为四个步骤。

  1. 建立一个 CRA 项目。

  2. 安装 Redux。

  3. 创建一个 Redux 循环:action、reducer 和 store。

  4. 提起诉讼。

我们开始吧。

你可以从本书的 GitHub 位置下载代码。

https://github.com/Apress/react-and-libraries/tree/master/05/hello-redux

和往常一样,如果您从 GitHub 下载这段代码,请记住运行以下代码:

$ yarn install
$ yarn start

并且检查 3000 端口的 app,换句话说就是http://localhost:3000/

步骤 1:设置项目

现在,让我们创建我们的项目。正如我们在第一章中所做的,我们将基于 CRA 创建一个新项目,并将其命名为hello-redux.

$ yarn create react-app hello-redux --template must-have-libraries

安装完成后,确保它在端口 3000 上正确运行,将目录更改为hello-redux,并使用以下命令运行它:

$ yarn start

接下来,我们将安装带有-D标志的 Redux,因此它会将 Redux 添加到package.json文件中。在撰写本文时,Redux 的版本是⁴.0.5.

$ yarn add redux -D

CRA·MHL 模板项目和我们在第二章安装的是 Redux 工具包。这就是为什么我们需要安装香草 Redux。

步骤 2:创建一个 Redux 循环

让我们以一个简单的用户手势为例。假设用户点击一个按钮,应用中需要一个数据更改。例如,用户想要登录或退出我们的应用。这意味着我们的应用需要两种状态:一个用户登录,一个用户注销。

这听起来简单而普通;然而,如果没有一个正式的机制来分离这些关注点,这可能会很快变得混乱。

我们的应用可能需要进行全面的数据更改,从更改菜单到更改页面信息,再到更改成员的访问权限等等。

此外,您的应用现在可能很简单,但随着代码的增长,您可能会添加其他状态,如注册用户、已登录的现有用户、用户交互超时等状态。

这就是 Redux 可以帮忙的地方。它有助于使你的应用更具可读性,更易于重构和维护。即使你现在不需要它,通过使用 Redux,你也在为你的应用未来的增长做准备。看一下图 5-4 中 Redux 的万尺图。

img/503823_1_En_5_Fig4_HTML.jpg

图 5-4

Redux 的 10000 英尺图(鸣谢:Medium.com)

如您所见,它与 Flux 架构保持一致。与其用这个图向你解释,不如让我们来看一下登录/注销状态。如果您想更好地理解应用和代码中发生的事情,请随时参考这个图表。看看代码,测试一下,跟着做,就有意义了。

首先,让我们创建文件夹层次结构。首先,我们将创建actionsreducersstore文件夹。在 Mac 上,使用以下命令:

$ mkdir -p src/redux/actions src/redux/reducers src/redux/store

Note

我正在使用-p标志。-p标志创建每个目录以及我们为了避免错误而创建的更高级目录(js)。

在面向 PC 用户 Window 终端上,使用以下命令:

$ md src/redux/actions src/redux/reducers src/redux/store

此时,文件夹结构应该如图 5-5 所示。

img/503823_1_En_5_Fig5_HTML.jpg

图 5-5

hello-redux 应用的文件夹结构

在 Redux 中,我们需要创建一个保存状态的存储。我们将很快创建 Redux 商店;坚持住。

重复操作

我们将从行动开始。创建一个新文件,并将其命名为src/js/actions/MenuActions.ts

存储保存状态树。更改状态树只能通过发出一个动作来完成;这是单向的。

一个物体描述了发生的事情。在我们的例子中,我们创建了两个状态,一个用于用户登录,另一个用于用户注销。我们的 reducer 在用户登录和注销时都会有逻辑。我们姑且称这些为showLoginshowLogout

// src/js/actions/MenuActions.ts

export const USER_LOGIN = 'USER_LOGIN'
export const USER_LOGOUT = 'USER_LOGOUT'

export function showLogin() {
  return { type: USER_LOGIN }
}

export function showLogout() {
  return { type: USER_LOGOUT }
}

还原剂

为了指定动作如何转换状态树,我们编写了 reducers。在我们的例子中,我们将有一个在状态改变时更新的菜单。创建一个新文件,并将其命名为src/js/reducers/MenuReducer.ts

Note

我们的 reducer 拥有控制应用状态的逻辑。

在代码级别,让我们创建一个默认的登录状态和一个开关来处理状态更改。

数据变更会将is_logintrue更新为false。这将使我们能够知道用户是否登录。

// src/js/reducers/MenuReducer.ts

import { USER_LOGIN, USER_LOGOUT } from '../actions/MenuActions'

const DEFAULT_LOGIN_STATE = {
  isLogin: false
}

export default (state = DEFAULT_LOGIN_STATE, action: { type: String }) => {
  switch (action.type) {
    case USER_LOGIN:
      return { is_login: true }
    case USER_LOGOUT:
      return { is_login: false }
    default:
      return state
  }
}

与我们将在接下来讨论的 Redux 工具包 不同,Redux 是“非个人化的”,因此我们在这里可以做任何我们想做的事情。我们可以设计我们的应用,并将所有代码放在一个文件中,如果这是我们想要的。或者,我们可以坚持更好的设计,让每个缩减器使用自己独立的、松散耦合的代码。

你怎么想呢?我更喜欢后者。

当我们构建我们的应用时,我们会有很多逻辑,所以最好将每个逻辑部分分成它自己的归约器。因为我们会有很多这样的减速器,我们能做的就是创建一个RootReducer对象来保存所有这些减速器。继续用下面的代码创建一个RootReducer.ts文件:

// src/js/reducers/RootReducer.ts

import { combineReducers } from 'redux'
import MenuReducer from './MenuReducer'

export default combineReducers({
  MenuReducer,
})

在这个设计中,我们可以将 reducer 添加到根文件中,根文件将作为我们的 reducer 的目录。

第三步:Redux 商店

正如我在 Flux 架构中提到的,我们应用的整个状态应该存储在一个单独的存储中的一个对象树中。我们准备好创建商店了。创建一个名为src/js/store/index.ts的文件。

在代码层面上,基于我们通过添加根 reducer 创建的 reducer 创建一个存储,它包含我们所有的 reducer。

// src/js/store/index.ts

import { createStore } from 'redux'
import rootReducer from '../reducers/RootReducer'

const index = createStore(rootReducer)

export default index

步骤 4:调用操作

让我们创建逻辑来处理我们的前端代码。我想让我们的例子尽可能简洁,所以让我们使用窗口来访问 DOM 文档。创建src/redux/store/index.ts

Window界面代表一个包含 DOM 文档的窗口。我们的例子只是为了学习,所以我使用Window接口只是为了显示逻辑,而不是显示它与实际组件的连接。

// src/redux/store/index.ts

import store from './store/index'
import { showLogin, showLogout } from './actions/MenuActions'

export interface StoreWindow extends Window {
  store: typeof store
  showLogin(): { type: string }
  showLogout(): { type: string }
}
declare let window: StoreWindow

window.store = store
window.showLogin = showLogin
window.showLogout = showLogout

Note

我声明StoreWindow是因为 TS 需要一个接口。这是因为 TS 需要定义类型。

在代码级别,我们所做的是设置存储和我们的函数来显示和隐藏登录状态。一旦我们订阅了商店的回调,我们就可以使用dispatch并看到状态的变化。

在我们的例子中,我们只是使用警报让您知道状态变化,但是任何人都可以订阅这些事件并更新视图。

最后,添加我们的import语句来运行我们的src/index.js

// src/index.js

import { StoreWindow } from './redux'

declare let window: StoreWindow
window.store.getState()
window.store.subscribe(() => window.alert(JSON.stringify(window.store.getState())))

window.store.dispatch(window.showLogin())
window.store.dispatch(window.showLogout())

在浏览器中,当状态改变时,您会收到两个警告。

{"MenuReducer":{"is_login":true}}
{"MenuReducer":{"is_login":false}}

图 5-6 显示了最终结果。

img/503823_1_En_5_Fig6_HTML.jpg

图 5-6

Redux hello-world 最终结果

让我们回顾一下

我们能够快速创建 Redux 数据移动,并理解 Redux 是怎么一回事。我们设置了 React 项目并安装了 Redux。我们创造了一个循环。最后,我们调用了一个动作。

Redux 工具包

正如我们所见,原始香草 Redux 风味是一个有用的工具。它帮助我们组织应用的数据和用户交互,因此我们的代码不会变得混乱。

我们有一个存储的概念,它可以保存我们的序列化键值对,并保存我们想要的任何值。

Redux 工具包 架构将每个功能分成一个特性片,随着越来越多的特性被添加,我们可以访问每个“片”数据。如果我们从食物的角度考虑,Redux 是一个香草冰淇淋蛋卷,然后我们的商店保存所有的数据或口味。

在我们的 Redux 工具包中,我们的商店更像一个比萨饼。我们的“切片”就像一片披萨,所有的切片组成了我们的商店。这就是所谓的鸭子模式(“re-dux”听起来像鸭子,懂吗?),所以每个功能都将存储在单独的文件夹中。

Redux 工具包 是固执己见的,我们被鼓励创建一个特性文件夹来保存所有不同的片段。Redux 工具包包括一些自以为是的 API,帮助我们更有效地使用 Redux。

Redux API 由以下主要函数组成:createSliceconfigureStorecreateReducercreateActioncreateAsyncThunkcreateEntityAdapter

其想法是提供样板工具,抽象设置过程,处理最常见的用例以及有用的实用程序,让用户简化编写代码。它负责中间件和 DevTools,而不需要做任何设置,并且它允许更容易的测试。

Redux 工具包 的目的是解决与 Redux 工具包.js 组织( https://redux-toolkit.js.org/introduction/quick-start ):

“配置 Redux 存储太复杂

——我必须添加许多包才能让 Redux 做任何有用的事情

——Redux 需要太多的样板代码"

Redux 工具包 在 React 企业级应用中被大量使用,这使得它成为高级到专家 React 开发人员的必备库,根据 React 团队的说法,“这些工具应该对所有 Redux 用户有益。”我个人同意,但我会让你来评判。

引擎盖下的 Redux 工具包

如果我们得到更多的技术,在引擎盖下,Redux 工具包 APIs 减少了我们需要编写的代码。有了样板代码,它可以防止我们犯常见的错误,因为代码会将错误减到最少。例如,常见的错误可能包括改变状态(在创建后更改数据并引入潜在的错误)或在状态中设置不可序列化的数据,如函数和承诺(而不是单独处理它们)。

这是通过redux-immutable-state-invariantserializable-state-invariant-middleware以及createAsyncThunk() API 来完成的,以处理不可序列化的数据异步动作,比如与您的 API 交互。这与函数式编程(FP)密切相关。正如我们在前面的章节中看到的,功能组件是 React 的主要构件之一。

FP 意味着我们正在用纯粹的功能构建我们的软件,避免共享状态、可变数据和副作用。FP 是声明性的而不是命令性的,应用状态流过纯函数。因为 React 是一种声明性语言(它实际上并不直接操纵 DOM 本身),所以它与 Redux 工具包 集成得很好,因为它有助于加强声明性架构。

Note

声明式编程表达了计算的逻辑,而没有描述它的控制流。命令式范式使用改变程序状态的语句,比如直接改变 DOM。

也就是说,如果你想创建自己的中间件,Redux 工具包 仍然有getDefaultMiddleware(),同时利用默认设置。getDefaultMiddleware()将在我们创建商店时被调用。

不会调用redux-immutable-state-invariant, serializable-state-invariant-中间件和createAsyncThunk(),需要自己处理逻辑。

Redux 工具包 也运行 Immer 库( https://github.com/immerjs/immer ),它让我们以一种变异的方式编写代码。我们通过简单地修改当前树来创建下一个不可变的状态树。我会在createSlice()代码里给你看。

我们真正想要的是使用尽可能多的纯功能组件。没有生命周期方法的组件要求我们依赖基于声明性的props方法,并提供性能改进,因为任何地方都不会有内存泄漏。

在纯函数方法中,对函数的每次调用都产生相同的结果,没有副作用。有道理吗?总的来说,这是很好的编程实践。

使用 Redux 工具包实现主题

现在我们已经介绍了 Redux 工具包,我们可以开始实现逻辑了。让我们创建一个首选项功能,我们可以在其中保存用户特定的首选项,如应用设置。

我们将从上一章停止的地方开始。

https://github.com/Apress/react-and-libraries/04/exercise-4-2

你可以从书的 GitHub 位置下载本章的完整代码。

https://github.com/Apress/react-and-libraries/05/exercise-5-1

一旦你运行了完整的代码,你就可以切换应用的主题,如图 5-1 和图 5-2 所示。

模型枚举对象:首选项对象

在创建切片之前,让我们创建一个枚举文件来保存我们的首选项。我们可以设置一个变量,用两个选项叫它ThemeEnum:darklight

首先,创建src/model/preferencesObject.ts

// src/model/preferencesObject.ts

export enum ThemeEnum {
  Dark = 'dark',
  Light = 'light',
}

模型索引文件

当我们访问我们的模型时,有一个简单的方法来访问这些对象。我们知道我们需要我们的包 JS chuck 中的全部对象内容,所以我们可以在 model 文件夹中创建一个类,它将包含我们创建的所有方法。

// src/model/index.ts
export * from './preferencesObject'

接下来,随着我们模型库的增长,我们可以以更直观的方式访问它们。

而不是这个:

import { ThemeEnum } from '../../model/preferencesObject'

我们可以这样访问我们的方法:

import { ThemeEnum } from '../../model'

这种方法有利也有弊。当我们优化我们的应用时,你会在第十二章学到更多关于优化 React 应用块的技术。

将主题设置为全局样式:index.scss

src/index.scss是放置主题颜色的好地方,因为它们与不止一个组件相关,并且是一种全局风格。

.dark {
  color: white;
  background-color: #2b2b2b;
}

.light {
  color: black;
  background-color: darkgrey;
}

创建一个状态切片:preferencesSlice.ts

接下来,创建一个文件并将其命名为preferencesSlice.ts。在切片内部,我们设置数据的初始状态。我把它设置为浅色(ThemeEnum.Light)作为初始状态。

对于减压器,我设置了三种类型的行动。

  • setThemeDark :将我们的应用主题设置为深色

  • setThemeLight :将我们的应用主题设置为浅色

  • 切换主题:切换到相反的配色方案

看一看:

// src/features/Preferences/preferencesSlice.ts

import { createSlice } from '@reduxjs/toolkit'
import { ThemeEnum } from '../../model’

interface SliceState {
  theme: ThemeEnum
}

createSlice是逻辑所在的地方,我们创建initialState以及减少器。

const preferences = createSlice({
  name: 'preferences',
  initialState: {
    theme: ThemeEnum.Light,
  } as SliceState,

我们的减速器保持动作。虽然我只需要使用switchTheme而不是其他方法来切换主题,但是我已经为我可能需要的其他动作创建了 reducers。在测试方面,当我们到第 X 章时,你可以看到这种逻辑是如何有意义的,因为每个动作都可以单独测试。

  reducers: {
    setThemeDark: (state, action) => {
      state.theme = ThemeEnum.Dark
    },
    setThemeLight: (state, action) => {
      state.theme = ThemeEnum.Light
    },
    switchTheme: (state, action) => {
      state.theme = state.theme === ThemeEnum.Light ? ThemeEnum.Dark : ThemeEnum.Light
    },
  },
})

最后,导出操作。

export const { setThemeDark, setThemeLight, switchTheme } = preferences.actions
export default preferences.reducer

Redux 工具包商店

现在我们已经有了带有特定动作的片段,我们可以设置Store.ts。我们所做的是用我们设置的片来配置存储。这就是我们所需要的。看一看:

// src/redux/store.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import prefSlice from '../features/Preferences/preferencesSlice'

const store = configureStore({
  reducer: combineReducers({
    preferences: prefSlice,
  }),
})

export default store

太好了。我们已经准备好了状态逻辑。我们现在可以实现视图了。在我们名为src/layout/Footer/Footer.tsx的页脚视图文件中,我们可以创建一个带有链接列表和按钮的页脚,该按钮将向切换颜色主题的操作发送回调。

页脚架构

至此,我们已经为页脚创建了一个占位符。我们的页脚需要知道用户的偏好,并能够更新主题。为了实现这一点,我们可以用父组件FooterTheme.jsx包装子组件Footer,就像我们对Header所做的那样。看看图 5-7 。

img/503823_1_En_5_Fig7_HTML.jpg

图 5-7

页脚组件的高级架构

页脚子组件

我们创建一个类型为FunctionComponent的类,并传递主题prop。然后我们可以将视图包装在导航标签中。这个逻辑将设置页脚的背景来改变整个容器。我们还使用styled-component库来提取组件的样式。

// src/layout/Footer/Footer.tsx

import React, { FunctionComponent } from 'react'
import classNames from 'classnames'
import { List, ListItem } from '@material-ui/core'
import { NavLink } from 'react-router-dom'
import InvertColorsIcon from '@material-ui/icons/InvertColors'
import { useStyles } from './Footer.styles'

// Redux 工具包
import store from '../../redux/store'
import { ThemeEnum } from '../../model’
import { switchTheme } from '../../features/Preferences/preferencesSlice'

const Footer: FunctionComponent<TFooterProps> = ({ theme }) => (
  <nav className={theme === ThemeEnum.Dark ? 'dark' : 'light'}>
    <NestedGrid />
  </nav>
)

export type TFooterProps = {

  theme: ThemeEnum
}

export default Footer

我们的NestedGrid函数可以接受主题prop并返回链接列表。

function NestedGrid() {
  const classes = useStyles()
  const footerClasses = classNames({
    [classes.footer]: true,
  })
  const aClasses = classNames({
    [classes.a]: true,
  })

updatePref方法将使用 Redux 工具包 片来产生一个动作,该动作将更新首选项的状态。

  const updatePref = () => {
    store.dispatch(switchTheme(store.getState()))
  }

然后我们可以在渲染函数return中设置这些样式类:

return (
  <footer className={footerClasses}>
    <div className={classes.container}>
      <div className={classes.left}>
        <List className={classes.list}>
          {[
            { name: 'Contact', url: '/Contact' },
            { name: 'About', url: '/About' },
            { name: 'Books', url: '/Books' },
            { name: 'Courses', url: '/BuildSiteCourse' },
          ].map((itemObject, index) => (
            <NavLink to={itemObject.url} className={classes.block} key={itemObject.url} activeClassName="NavLinkItem-selected">
              <ListItem className={classes.inlineBlock}>{itemObject.name}</ListItem>
            </NavLink>
          ))}
        </List>
      </div>
      <div className={classes.right}>
        &copy; {new Date().getFullYear()}{' '}

下面是我们的按钮,它使用商店并调度一个回调来切换我们在切片中创建的主题动作:

        <button type="submit" onClick={() => updatePref()} className={aClasses}>
          <InvertColorsIcon className={classes.icon} /> Change theme to {store.getState().preferences.theme === ThemeEnum.Dark ? 'light' : 'dark'}
        </button>
      </div>
    </div>
  </footer>
)

Footer.styles.ts 样式组件

注意,我们在Footer.tsx子组件中调用了Footer.styles.ts。我们将视图和风格分开,并将风格放在一个名为Footer.styles.ts的单独文件中。我们的代码可以导入我们的样式文件。

import { useStyles } from './Footer.styles'.

然后使用类的对象名,我们可以设置链接的字体颜色。如果背景是暗的,我们需要亮色字体,如果背景是亮的,我们需要深色字体。

Footer.styles.ts中,我们的代码可以设置链接的样式,甚至包括媒体查询,以使用 Material-UI 核心 APIcreateStylesmakeStylesTheme调整我们的页脚容器。

import { createStyles, makeStyles, Theme } from '@material-ui/core'

export const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    block: {
      color: 'inherit',
      padding: '0.9375rem',
      fontWeight: 500,
      fontSize: '12px',
      textTransform: 'uppercase',
      borderRadius: '3px',
      textDecoration: 'none',
      position: 'relative',
    },
    left: {
      display: 'block',
    },
    right: {
      padding: '15px 0',
      margin: '0',
    },
    footer: {

      padding: '0.9375rem 0',
      textAlign: 'center',
      display: 'flex',
      zIndex: 2,
      position: 'relative',
    },
    a: {
      color: '#9c27b0',
      textDecoration: 'none',
      backgroundColor: 'transparent',
    },
    footerWhiteFont: {
      '&,&:hover,&:focus': {
        color: '#FFFFFF',
      },
    },
    container: {
      paddingRight: '15px',
      paddingLeft: '15px',
      marginRight: 'auto',
      marginLeft: 'auto',
      width: '100%',
      '@media (min-width: 576px)': {
        maxWidth: '540px',
      },
      '@media (min-width: 768px)': {
        maxWidth: '720px',
      },
      '@media (min-width: 992px)': {
        maxWidth: '960px',
      },
      '@media (min-width: 1200px)': {
        maxWidth: '1140px',
      },
    },
    list: {
      marginBottom: '0',
      padding: '0',
      marginTop: '0',
    },
    inlineBlock: {
      display: 'inline-block',
      padding: '0px',
      width: 'auto',
    },
    icon: {
      width: '18px',
      height: '18px',
      position: 'relative',
      top: '3px',
    },
  })
)

最后,我们需要用名为FooterTheme.tsx的父组件包装我们的Footer子组件,如果用户决定切换主题,父组件将监听来自商店的调度回调。该架构允许我们从任何视图调度回调,而不是从页脚强制进行交互。

FooterTheme 包装父组件

为了实现这一点,我们将FooterTheme.tsx创建为FunctionComponent并订阅回调。

  store.subscribe(() => {
    setTheme(store.getState().preferences.theme)
  })

下面是完整的 FooterTheme.tsx 代码;

// src/layout/Footer/FooterTheme.tsx

import React, { FunctionComponent, useState } from 'react'
import { ThemeEnum } from '../../model’
import store from '../../redux/store'
import FooterComponent from './Footer'

export const FooterTheme: FunctionComponent = () => {
  const [theme, setTheme] = useState<ThemeEnum>(ThemeEnum.Light)
  store.subscribe(() => {
    setTheme(store.getState().preferences.theme)
  })
  return <FooterComponent theme={theme} />
}

HeaderTheme 父组件

我们需要为标题做同样的事情。对于头部,我们已经设置了HeaderTheme包装器父组件,并设置了appBarBackgroundStyle方法。对于HeaderTheme.tsx来说,代码重构的变化是突出的。

// src/layout/Header/HeaderTheme.tsx

import React, { FunctionComponent, useState } from 'react'
import AppBar from '@material-ui/core/AppBar/AppBar'
import { useMediaQuery } from '@material-ui/core'
import HeaderComponent from './Header'
import store from '../../redux/store'
import { ThemeEnum } from '../../model’

function appBarBackgroundStyle(color: string) {
  return {
    background: color,
  }
}

我们使用 React useState方法来设置状态。然后,我们可以为AppBar设置样式,并将状态传递给HeaderComponent。看一看:

export const HeaderTheme: FunctionComponent = () => {
  const smallBreakPoint = useMediaQuery('(min-width: 0px) and (max-width: 1100px)')
  const [theme, setTheme] = useState<ThemeEnum>(ThemeEnum.Light)
  store.subscribe(() => {
    setTheme(store.getState().preferences.theme)
  })

  return (
    <AppBar position="fixed" style={appBarBackgroundStyle(theme === ThemeEnum.Dark ? '#2b2b2b' : 'white')}>
      <HeaderComponent theme={store.getState().preferences.theme} smallBreakPoint={smallBreakPoint} />
    </AppBar>

  )
}

标题子组件

需要重构Header组件,以便能够保存主题状态的属性,并将状态传递给不同的子组件,并更新 Material-UI 组件的状态。

让我们来看看。重构IHeaderProps接口,这样我们可以从HeaderTheme父组件传递主题。这里突出显示了代码更改:

import { ThemeEnum } from '../../model'

interface IHeaderProps {

  theme: ThemeEnum
  smallBreakPoint: boolean
}

为了设置按钮的颜色,我们将使用一个名为menuLabelBackgroundStyle的方法来传递颜色编号。

function menuLabelBackgroundStyle(color: string) {
  return {
    color,
    padding: 20,
  }
}

现在我们的材质界面按钮能够在props更新时切换颜色。

<Button component={Link} style={menuLabelBackgroundStyle(this.props.theme === ThemeEnum.Dark ? 'white' : 'black')} to="/">
  ELI ELAD ELROM
</Button>

我们还需要将prop主题变量传递给两个叫做HeaderTopNavHeaderDrawerHeader子组件。

<HeaderTopNav theme={this.props.theme} />
<HeaderDrawer theme={this.props.theme} />

HeaderTopNav 子组件

对于我们的HeaderTopNav子组件,我们需要将prop添加到接口中。

import { ThemeEnum } from '../../model'

interface IHTNavProps {
  theme: ThemeEnum
}

我们将使用与在Header组件中相同的方法来设置材质 UI 按钮的样式。由于它与Header组件中的代码相同,我们可以将代码提取到一个实用程序中;然而,由于代码很小,我更喜欢保持它的副本,这样每个组件都可以作为一个独立的组件提取出来,没有其他的依赖,但是提取重复的代码并不是一个坏主意。

function menuLabelBackgroundStyle(color: string) {
  return {
    color,
    padding: 20,
  }
}

接下来,我们可以为每个父按钮设置样式,并为每个按钮重复这个过程:联系人、资源、指导和建立我的网站。我只显示了一个按钮,但会为每个按钮重复相同的样式属性。

<Button style={menuLabelBackgroundStyle(this.props.theme === ThemeEnum.Dark ? 'white' : 'black')} onClick={(event) => this.handleMenuOpen(event, '1')}>
  Build My Website
</Button>

HeaderDrawer 子组件

对于headerDrawer,我现在不打算实现任何样式更改,但是我想为以后的更改做准备,所以我将让子组件通过props知道状态,就像我们在HeaderTopNav中做的那样。

import { ThemeEnum } from '../../model'

interface IHDProps {
  theme: ThemeEnum
}

适当的重构

既然我们已经重构了我们的页脚,我们需要改变AppRouter.tsx来使用<FooterTheme />子组件而不是<Footer />。看一看:

const AppRouter: FunctionComponent = () => {
  return (
    <Provider store={store}>
      <Router>
        <HeaderTheme />
        <Switch>
        ...
        </Switch>
        <div className="footer">
          <FooterTheme />
        </div>
      </Router>
    </Provider>
  )
}
export default AppRouter

恭喜你!我们完成了编码。如果您检查应用,您应该会看到如图 5-1 和图 5-2 所示的结果。

因为我们的页面不包含任何内容,所以我们可以调整页面的 SCSS 高度,使其看起来整洁。

// src/App.scss
.App {
  text-align: center;
  padding-top: 120px;
  height: 350px;
}

我们现在有了一个包含导航、页面、抽屉、主题和状态管理的站点脚手架,能够在组件和子组件之间传递状态。

为了保证质量,正如我们在上一章中所做的那样,您应该养成运行以下程序的习惯:

$ yarn format
$ yarn lint
$ yarn test

这将确保我们通过测试,林挺,并解决任何问题。本例中的代码通过了所有的测试,所以您可以将您的最终结果与我的进行比较。

redux devtools extension(redux devtools 扩展)

我想向您展示的最后一件事将是添加到 React 工具箱的一个很好的附加功能,它是一个跟踪 Redux 工具包 中发生的事情的插件。它被称为 Redux DevTools 扩展。对于 Chrome,你可以从这里下载:

https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en

此外,您也可以随意访问 GitHub 上的项目,获取其他浏览器的插件。

https://github.com/zalmoxisus/redux-devtools-extension

当我们使用我们构建的应用时,我们现在可以看到插件上正在调度的操作,如图 5-8 所示。点击链接切换我们应用的主题。在我们的例子中,应用的主题从亮变暗。随着您添加越来越多的切片和代码的增长,这个工具会派上用场。

img/503823_1_En_5_Fig8_HTML.jpg

图 5-8

Chrome 的 Redux DevTools 扩展

摘要

在这一章中,你学习了状态管理和脸书提出的状态管理架构 Flux。通过使用 CRA 创建一个hello-redux应用,您了解了在撰写本文时最流行的状态管理方法 Redux。然后你学习了 Redux 工具包,并在这个过程中在我们上一章开始构建的应用中实现了 Redux 工具包。在 Redux 工具包 和样式化组件的帮助下,我们添加了主题和使用 Flux 状态管理在不同组件之间进行状态通信的能力。最后,我们为 Chrome 安装了 Redux DevTools 扩展。

在下一章中,我们将继续构建我们的应用,并使用反冲和 Mongo-Express-React-node . js(MERN)栈实现一个具有独占私有成员区域的登录。