原文地址:https://www.sitepoint.com/replace-redux-react-hooks-context-api/

当下最受欢迎的用于管理React应用状态的库莫过于Redux了。而在React的新特性中包含了React Hooks和Context API,这俩个特性有效的消除了许多React大型项目中开发者面临的问题。其中最大的一个问就是“prop drilling”(主要指层级复杂的组件通过props一层一层的传递数据),这些在一些多层级嵌套的组件中非常常见。通用的解决方案就是使用像Redux这样的状态管理库。但很不幸的是,使用它意味着需要额外的写很多类似的模版代码。但现在,我们已经可以使用React HooksContext API来取代繁琐的Redux的写法了。
在这篇文章中,我们会学会一种新的React项目中管理状态的方式,而且不需要通过安装第三方库——比如Redux。React hooks允许我们在函数组件中使用内部状态,与此同时Context API可以让我们更好的共享组件之间的状态。

先决条件

为了能更好的掌握此教程,你需要对以下相关主题比较熟悉:

本文介绍的是一种基于Redux的模式,这意味着你需要非常数据reducers和actions的概念。我使用的IDE是VS Code,如果你使用的是windows,我推荐你安装以下Git Bash,并使用Git Bash终端,来执行本文中的所有命令。Cmder也是一个优秀的终端可以在Windows系统中执行大部分的Linux命令。
完整的工程在这里: GitHub Repository.

关于新的状态管理

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

  • 内部状态
  • 全局状态

内部状态只允许在组件内部定义和使用。全局状态可以在多个组件中共享使用。在以前,使用全局状态需要安装状态管理的框架如:Redux或MobX。在React v16.3.0 中发布了 Context API ,它允许开发者实现简单的全局的状态管理而不需要使用任何第三方的库。
到了React v16.8,Hooks 支持了函数式组件中也可以实现类组件中的一系列特性。Hooks给React开发者编写代码带来了巨大的便利。包括代码复用、更简单的组件间共享状态的方式。在本篇教程中,我们会关注到以下的React hooks:

useState 一般推荐用于管理一些简单的数值或字符串类型的状态。如果需要管理一些复杂的数据结构,你可能需要用到useReducer hook.对于useState 你只需要一个简单的函数setValue() 就可以改变已有的状态值。
而如果使用 useReducer, 你就需要管理的状态可能就会是一个包含多种数据组成的,类似树形结构的数据集合。你需要定义用于改变这些状态的具体action函数。如果你的数据类型是数组,你就需要定义多个immutable functions (不可变函数)来管理数组的新增、修改和删除的方法。你也会在稍后的章节中看到类似的例子。
一旦你通过 useStateuseReducer 定义和共享状态,你就需要使用React Context将其提升为全局状态. 我们可以通过使用React提供的createContext创建一个Context对象来达到这个目的. Context 对象不需要通过props传递,就可以可以在多个组件中共享状态。
你也需要使用定义一个Context Provider。它可以让页面或者一个容器组件来订阅Context对象的改变,这使得任意一个子组件都可以通过useContext函数来获取Context对象的数据。
接下来,我们来看一看具体的代码:

创建项目

我们使用create-react-app 来快速创建我们的项目。

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

接着我们安装一下 Semantic UI React,一个基于React的CSS框架。这不是必须的;我只是想要快速创建一个好看的交互界面,而且不需要写太多额外的代码:

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

打开 src/index.js 添加以下的引入:

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

在后面的章节中,我们会通过useState定义一些状态,并把它放到全局状态中。

计数器例子: useState

在这个例子中,我们会创建一个计数器的demo. 我们会创建一个 count 的状态,它会在两个组件中共享. 我们会创建一个 CounterView的组件,它会做为一个容器组件。容器组件内部会有一个按钮组件,按钮组件内部会有两个按钮, 这两个按钮分别可以增加和减少状态:count 的值。
我们一开始在 context/counter-context.js文件中先定义状态: count 。如下代码

  1. import React, { useState, createContext } from "react";
  2. // 创建Context对象
  3. export const CounterContext = createContext();
  4. // 创建provider用于消费和监听context的改变
  5. export const CounterContextProvider = props => {
  6. const [count, setCount] = useState(0);
  7. return (
  8. <CounterContext.Provider value={[count, setCount]}>
  9. {props.children}
  10. </CounterContext.Provider>
  11. );
  12. };

我们已经定义了一个叫做count 的状态,并且设置了默认值0。所有消费CounterContext.Provider 的组件都可以使用提供的count值和setCount方法。我们在定义一个用于显示count值的组件 src/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>
  );
}

接着:我们再定义包含两个按钮用于增加和减少count值的组件: src/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>
  );
}

接着我们需要定义一个容器组件来包裹我们的组件src/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>
  );
}

你可以在下面的codepen例子中点击查看:
点击查看【codepen】
我们继续看看下一个章节,后面的内容会更高级一点,我们使用了useReducer hook。

联系人例子: useReducer

在这个例子中,我们会构建一个简单的CRUD的页面,用于管理联系人。它会有一些列的展示组件和容器组件构成。其中也会包含一个用于管理联系人状态的Context对象.我们的状态树里的对象值也会比以往的例子复杂一些, 所以我们需要使用 useReducer hook。
通过 src/context/contact-context.js 创建一个Context Object

import React, { useReducer, createContext } from "react";
// 联系人Context对象
export const ContactContext = createContext();
// 初始值,包含contacts、laoding、error三个状态
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
};
// reducer,包含集中动作新增联系人、删除联系人、开始、结束
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>
  );
};

创建的容器组件如下:

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

用于展示联系人信息的组件:src/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() {
  // 获取状态 `contacts` 和改变状态的函数:dispatch
  const [state, dispatch] = useContext(ContactContext);
  // 组件内部状态
  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>
  );
}

表单组件:src/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
  };
}

入口文件的代码如下:

import React from "react";
import { Container } from "semantic-ui-react";
import ContactView from "./views/contact-view";

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

你可以在下面的链接中验证相关的例子:https://cdpn.io/SitePoint/fullembedgrid/zYqjLLb?animations=run&type=embed
请仔细查阅以下我提供的代码,以及我提供的注释,确保你都了解了我想表达的内容。

总结

我希望这些例子能够更好的帮助你了解:不借助任何第三方框架如何在组件间共享数据状态。如果不使用hooks和context API,你可能需要额外编写更多的代码。
你也意识到第二个例子没有使用到loadingerror 这两个状态。做为一个挑战,你可以接着使用这些状态来完善第二个例子。比如你可以给具体实现添加一个延迟,让表格在加载时呈现一个loading的状态。你也可以使用一个真实的远程API来实现真正的CRUD。当遇到加载错误时,就会用到error状态来给予用户相应的反馈.
唯一你需要问问你自己的问题是。Redux在未来的项目中真的需要吗?目前我看到这种技术的一个缺点是不能使用redux devtool扩展来调试应用程序状态.然而随着新的工具的开发,这种情况在未来将会发生变化。显然,作为开发人员,您仍然需要学习Redux,以便维护老的项目. 但是如果你正在开始一个新的项目,你需要问问自己和你的团队,使用第三方库来管理组件状态是否真的必要。