• 赚钱入口【需求资源】限时招募流量主、渠道主,站长合作;【合作模式】CPS长期分成,一次推广永久有收益。主动打款,不扣量;

如何用React Hooks和Context API替换Redux

JavaScript rin, seun 1年前 (2020-06-16) 425次浏览 0个评论

React Hooks和Context API替换Redux

在React中处理共享应用程序状态的最流行方法是使用诸如Redux之类的框架。最近,React团队引入了几个新功能,其中包括React Hooks和Context API。这两个功能有效地消除了大型React项目开发人员面临的许多挑战。最大的问题之一是嵌套组件中常见的“道具钻孔”。解决方案是使用像Redux这样的状态管理库。不幸的是,这付出了编写样板代码的代价—但是现在,可以用React Hooks和Context API替换Redux。

在本文中,您将学习一种在React项目中处理状态的新方法,而无需编写过多的代码或安装大量的库-Redux就是这种情况。React挂钩允许您使用功能组件内部的本地状态,而Context API则允许您与其他组件共享状态。

先决条件

您将在此处学习的技术基于Redux中引入的模式。这意味着您需要对进行深入了解,reducers然后actions再继续。我目前正在使用Visual Studio Code,它似乎是目前最受欢迎的代码编辑器(尤其是对于JavaScript开发人员而言)。如果您使用Windows,则建议您安装Git Bash。使用Git Bash终端执行本教程中提供的所有命令。Cmder也是一个很好的终端,能够在Windows上执行大多数Linux命令。

您可以访问本教程中用于GitHub存储库的完整项目。

关于新的状态管理技术

在React项目中我们需要处理两种状态:

  • 地方政府
  • 全球状态

局部状态只能在定义它们的组件内使用。全局状态可以在多个组件之间共享。以前,定义全局状态需要安装状态管理框架,例如Redux或MobX。随着React v16.3.0的发布,上下文API允许开发人员无需安装其他库即可实现全局状态。

React v16.8开始Hooks允许在组件中实现许多React功能,而无需编写类。Hooks为React开发人员编写代码的方式带来了巨大的好处。这包括代码重用和在组件之间共享状态的更简便方法。对于本教程,我们将关注以下React钩子:

  • useState
  • useReducer

useState建议用于处理数字或字符串之类的简单值。但是,在处理复杂的数据结构时,您将需要useReducer挂钩。对于useState,您只需要一个setValue()函数即可覆盖现有状态值。

对于useReducer,您将处理一个状态对象,该状态对象以树状结构包含多个具有不同数据类型的值。您将需要声明可以更改一个或多个这些状态值的函数。对于数组等数据类型,您将需要声明多个不可变的函数来处理添加,更新和删除操作。您将在本文后面的部分中看到一个示例。

使用useState或声明状态后useReducer,您需要使用React Context将其提升为全局状态。这是通过使用React库提供的功能创建一个上下文对象来完成的createContext。上下文对象允许状态在组件之间共享,而无需使用道具。

您还需要为上下文对象声明一个上下文提供程序。这允许页面或容器组件订阅上下文对象以进行更改。容器的任何子组件都可以使用该useContext函数访问上下文对象。闲聊,让我们看看实际的代码。

设置项目

我们将使用create-react-app快速启动我们的项目。

$ npx create-react-app react-hooks-context-app

接下来,让我们安装Semantic UI React,这是一个基于React的CSS框架。这不是必须的,我只是喜欢创建漂亮的用户界面而无需编写自定义CSS。

yarn add semantic-ui-react fomantic-ui-css

打开index.js并插入以下导入:

import 'fomantic-ui-css/semantic.min.css';

这就是我们的项目开始使用语义UI所要做的所有工作。在下一节中,我们将研究如何使用useState钩子声明状态并将其提升为全局状态。

反例– useState

对于此示例,我们将构建一个简单的计数器演示,该演示由两个按钮组件和一个显示组件组成。我们将介绍一个count将在两个组件之间全局共享的状态。组件将是其子级CounterView,将充当容器。按钮组件将具有将增加或减少count状态值的按钮。

让我们首先count在名为的上下文文件中定义状态context/counter-context.js。插入以下代码:

import React, { useState, createContext } from "react";

// Create Context Object
export const CounterContext = createContext();

// Create a provider for components to consume and subscribe to changes
export const CounterContextProvider = props => {
  const [count, setCount] = useState(0);

  return (
    <CounterContext.Provider value={[count, setCount]}>
      {props.children}
    </CounterContext.Provider>
  );
};Get the book free

我们定义了一个名为的状态count,并将默认值设置为0。所有使用的组件CounterContext.Provider都可以访问count状态和setCount功能。让我们在中定义用于显示count状态的组件components/counter-display.js

import React, { useContext } from "react";
import { Statistic } from "semantic-ui-react";
import { CounterContext } from "../context/counter-context";

export default function CounterDisplay() {
  const [count] = useContext(CounterContext);

  return (
    <Statistic>
      <Statistic.Value>{count}</Statistic.Value>
      <Statistic.Label>Counter</Statistic.Label>
    </Statistic>
  );
}

接下来,让我们定义组件,该组件将包含用于增加和减少state组件的按钮。创建文件components/counter-buttons.js并插入以下代码:

import React, { useContext } from "react";
import { Button } from "semantic-ui-react";
import { CounterContext } from "../context/counter-context";

export default function CounterButtons() {
  const [count, setCount] = useContext(CounterContext);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <div>
      <Button.Group>
        <Button color="green" onClick={increment}>
          Add
        </Button>
        <Button color="red" onClick={decrement}>
          Minus
        </Button>
      </Button.Group>
    </div>
  );
}

实际上,useContext由于我们尚未指定Provider,因此该功能将无法使用。现在,通过在中创建一个容器来进行操作views/counter-view.js。插入以下代码:

import React from "react";
import { Segment } from "semantic-ui-react";

import { CounterContextProvider } from "../context/counter-context";
import CounterDisplay from "../components/counter-display";
import CounterButtons from "../components/counter-buttons";

export default function CounterView() {
  return (
    <CounterContextProvider>
      <h3>Counter</h3>
      <Segment textAlign="center">
        <CounterDisplay />
        <CounterButtons />
      </Segment>
    </CounterContextProvider>
  );
}

最后,让我们用以下代码替换其中的现有代码App.js

import React from "react";
import { Container } from "semantic-ui-react";

import CounterView from "./views/counter-view";

export default function App() {
  return (
    <Container>
      <h1>React Hooks Context Demo</h1>
      <CounterView />
    </Container>
  );
}

现在,您可以create-react-app使用命令启动服务器yarn start。浏览器应启动,这将呈现一个相似的视图。单击按钮以确保incrementdecrement功能正常运行:

如何用React Hooks和Context API替换Redux

让我们进入下一部分,在这里我们将设置一个使用useReducer挂钩的高级示例。

联系人示例– useReducer

在此示例中,我们将构建一个用于管理联系人的基本CRUD页面。它由几个演示组件和一个容器组成。也将有一个用于管理联系人状态的上下文对象。由于我们的状态树比前面的示例要复杂一些,因此我们必须使用useReducer钩子。

创建状态上下文对象context/contact-context.js并插入以下代码:

import React, { useReducer, createContext } from "react";

export const ContactContext = createContext();

const initialState = {
  contacts: [
    {
      id: "098",
      name: "Diana Prince",
      email: "diana@us.army.mil"
    },
    {
      id: "099",
      name: "Bruce Wayne",
      email: "bruce@batmail.com"
    },
    {
      id: "100",
      name: "Clark Kent",
      email: "clark@metropolitan.com"
    }
  ],
  loading: false,
  error: null
};

const reducer = (state, action) => {
  switch (action.type) {
    case "ADD_CONTACT":
      return {
        contacts: [...state.contacts, action.payload]
      };
    case "DEL_CONTACT":
      return {
        contacts: state.contacts.filter(
          contact => contact.id !== action.payload
        )
      };
    case "START":
      return {
        loading: true
      };
    case "COMPLETE":
      return {
        loading: false
      };
    default:
      throw new Error();
  }
};

export const ContactContextProvider = props => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <ContactContext.Provider value={[state, dispatch]}>
      {props.children}
    </ContactContext.Provider>
  );
};

创建父组件views/contact-view.js并插入以下代码:

import React from "react";
import { Segment, Header } from "semantic-ui-react";
import ContactForm from "../components/contact-form";
import ContactTable from "../components/contact-table";
import { ContactContextProvider } from "../context/contact-context";

export default function Contacts() {
  return (
    <ContactContextProvider>
      <Segment basic>
        <Header as="h3">Contacts</Header>
        <ContactForm />
        <ContactTable />
      </Segment>
    </ContactContextProvider>
  );
}

创建演示文稿组件components/contact-table.js并插入以下代码:

import React, { useState, useContext } from "react";
import { Segment, Table, Button, Icon } from "semantic-ui-react";
import { ContactContext } from "../context/contact-context";

export default function ContactTable() {
  // Subscribe to `contacts` state and access dispatch function
  const [state, dispatch] = useContext(ContactContext);
  // Declare a local state to be used internally by this component
  const [selectedId, setSelectedId] = useState();

  const delContact = id => {
    dispatch({
      type: "DEL_CONTACT",
      payload: id
    });
  };

  const onRemoveUser = () => {
    delContact(selectedId);
    setSelectedId(null); // Clear selection
  };

  const rows = state.contacts.map(contact => (
    <Table.Row
      key={contact.id}
      onClick={() => setSelectedId(contact.id)}
      active={contact.id === selectedId}
    >
      <Table.Cell>{contact.id}</Table.Cell>
      <Table.Cell>{contact.name}</Table.Cell>
      <Table.Cell>{contact.email}</Table.Cell>
    </Table.Row>
  ));

  return (
    <Segment>
      <Table celled striped selectable>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Id</Table.HeaderCell>
            <Table.HeaderCell>Name</Table.HeaderCell>
            <Table.HeaderCell>Email</Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>{rows}</Table.Body>
        <Table.Footer fullWidth>
          <Table.Row>
            <Table.HeaderCell />
            <Table.HeaderCell colSpan="4">
              <Button
                floated="right"
                icon
                labelPosition="left"
                color="red"
                size="small"
                disabled={!selectedId}
                onClick={onRemoveUser}
              >
                <Icon name="trash" /> Remove User
              </Button>
            </Table.HeaderCell>
          </Table.Row>
        </Table.Footer>
      </Table>
    </Segment>
  );
}

创建演示文稿组件components/contact-form.js并插入以下代码:

import React, { useState, useContext } from "react";
import { Segment, Form, Input, Button } from "semantic-ui-react";
import _ from "lodash";
import { ContactContext } from "../context/contact-context";

export default function ContactForm() {
  const name = useFormInput("");
  const email = useFormInput("");
  // eslint-disable-next-line no-unused-vars
  const [state, dispatch] = useContext(ContactContext);

  const onSubmit = () => {
    dispatch({
      type: "ADD_CONTACT",
      payload: { id: _.uniqueId(10), name: name.value, email: email.value }
    });
    // Reset Form
    name.onReset();
    email.onReset();
  };

  return (
    <Segment basic>
      <Form onSubmit={onSubmit}>
        <Form.Group widths="3">
          <Form.Field width={6}>
            <Input placeholder="Enter Name" {...name} required />
          </Form.Field>
          <Form.Field width={6}>
            <Input placeholder="Enter Email" {...email} type="email" required />
          </Form.Field>
          <Form.Field width={4}>
            <Button fluid primary>
              New Contact
            </Button>
          </Form.Field>
        </Form.Group>
      </Form>
    </Segment>
  );
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  const handleChange = e => {
    setValue(e.target.value);
  };

  const handleReset = () => {
    setValue("");
  };

  return {
    value,
    onChange: handleChange,
    onReset: handleReset
  };
}

相应地插入以下代码App.js

import React from "react";
import { Container } from "semantic-ui-react";
// import CounterView from "./views/counter-view";
import ContactView from "./views/contact-view";
export default function App() {
  return (
<Container>
  <h1>React Hooks Context Demo</h1>
  {/* <CounterView /> */}
  <ContactView />
</Container>
  );
}

实施代码后,您的浏览器页面应刷新。要删除联系人,您需要先选择一行,然后点击“删除”按钮。要创建新联系人,只需填写表格并点击“新联系人”按钮。

如何用React Hooks和Context API替换Redux

查看代码以确保您了解所有内容。阅读我包含在代码中的注释。

摘要

我希望这些示例能使您更好地了解如何在不使用Redux的情况下管理共享应用程序状态。如果您不使用钩子和上下文API来重写这些示例,则将导致更多的代码。看到不处理道具就编写代码有多容易?

您可能已经在第二个示例中注意到,有几个未使用的状态变量,即loadingerror。作为挑战,您可以进一步改进此应用程序以利用它们。例如,您可以实施虚假延迟,并使演示文稿组件显示加载状态。您还可以更进一步,并访问真正的远程API。在此error状态变量可用于显示错误消息。

您现在可能要问自己一个唯一的问题:Redux对将来的项目是否必要?我已经用这种技术看到的一个缺点是您不能使用Redux DevTool扩展来调试应用程序状态。但是,将来随着新工具的开发,这种情况可能会改变。显然,作为开发人员,您仍然需要学习Redux才能维护旧项目。但是,如果您要开始一个新项目,则需要询问您自己和您的团队,是否真的有必要使用第三方状态管理库。

喜欢 (0)

您必须 登录 才能发表评论!