原文:Learn React Hooks – A Beginner's Guide,作者:Victor Ikechukwu

函数组件(Functional components)并不总是 React 中声明组件的首选方法。

在 React 16.8 版本推出之前,函数组件被视为二等公民。它们不能处理状态(state)、逻辑(logic)和许多其他的 React 特性,而且我们只用它们来向 UI 渲染非常简单的组件。

React 16.8 版通过引入 React Hooks 解决了这些问题,它让开发者在函数组件(Functional components)中使用这些 React 特性。

在这篇文章中,你将学习:

  • 什么是 React Hooks
  • 四个常见的 React Hooks,并举例说明如何在你的应用程序中编写它们
  • 最后,我们将看看如何编写你自己的自定义 React Hooks

什么是 React Hooks

Hooks 是 React 16.8 版本中引入的内置 React 函数。它们允许你在函数组件(Functional components)中使用 React 库的功能,如生命周期方法(lifecycle methods)、状态(state)和上下文(context ),而不必担心将其重写为一个类(class)。

每个 React Hook 名称的前缀是 "use" 一词。例如,useStateuseEffect。选择这种格式是因为 Hooks 让开发者使用 React 库的特殊功能。所以你是在use React 库的那个特殊功能。

为什么使用 React Hooks

许多开发者对学习 React Hooks 持怀疑态度,但你不应该这样。以下是你应该开始使用 React Hooks 的几个原因:

React 中的类可能是相当混乱的

类是正确学习 React 的一个障碍。为了使用它们,你需要了解this关键字是如何工作的。你还需要不断记住绑定事件处理程序,以及在 React 中使用类时遇到的其他多余的方法。

类组件(Classes components)部分很复杂,可能难以理解

类组件通常很大,并试图进行许多操作。从长远来看,它们变得难以理解。

Hooks 解决了这个问题,它允许你把大的组件分离成各种小的功能,而不是把所有的逻辑都强加到一个单一的组件中。

Hooks 的组件更短,可读性更好

类组件带有大量的模板代码。考虑一下下面的计数器组件:

class Counter extends Component {
    constructor(props) {
        super(props)
        this.state = {
         count: 1,
        }
    }
    render() {
        return (
            <div>
                The Current Count: {this.state.count}
                <div>
                <button onClick={this.setState({ count: this.state.count - 1 })}>
                add
                </button>
                <button onClick={this.setState({ count: this.state.count + 1 })}>
                subtract
                </button>
                </div>
            </div>
    );
    }
}

下面是使用函数组件(functional component)和 React Hooks 的等效代码:

function Counter  ()  {
    const [count, setCount] = useState(1);
    return (
        <div>
            The Current Count: {this.state.count}
            <div>
                <button onClick={() => setCount(count + 1)}>add</button>
                <button onClick={() => setCount(count - 1)}>subtract</button>
            </div>
        </div>
    );
};

请注意,类组件要复杂得多。你需要一个类来扩展 React,一个构造函数来初始化状态,而且你需要到处引用this 关键字。

使用函数组件(functional components)可以消除很多这些,所以我们的代码变得更短,更容易阅读和维护。

使用 React Hooks 的规则

在使用 React Hooks 时,有几个规则需要遵守:

  • 只在组件的顶层调用 Hooks:你不应该在循环、条件或嵌套函数中使用 Hooks。相反,总是在你的 React 函数的顶层使用 Hooks,在任何 return 关键字之前。
  • 只从React函数中调用Hooks:不要从普通的 JavaScript 函数中调用 Hooks。你可以:
    ✅ 从 React 函数组件(functional components)中调用 Hooks
    ✅ 从自定义 Hooks 中调用 Hooks

最常见的 React Hooks

到目前为止,React 有 10 个内置 Hooks。让我们看看四个最常见的 Hooks:

  • useState
  • useEffect
  • useContext
  • useReducer

useState Hook

useState Hook 允许你在函数组件(functional components)内创建、更新和操作状态。

React 有一个状态的概念,它是持有我们的组件所依赖的数据的变量,并可能随着时间的推移而改变。每当这些变量发生变化时,React 就会用状态变量的当前值重新渲染 DOM 中的组件来更新 UI。

这个钩子需要一个可选的参数,一个状态(state)的初始值。然后它返回一个包含两个值的数组:

  • state 变量
  • 一个用于更新状态的函数

让我们以一个计数器组件为例来看看:

要使用一个 Hook,第一步是在文件的顶部导入 Hook:

import { useState } from "react";

然后,用一个值初始化 Hook。由于它返回一个数组,你可以使用数组解构来访问数组中的各个项目,就像这样:

const [count, setCount] = useState(0);

有了这个,该组件的代码将是:

import { useState } from "react";

function Counter() {
    // Declare a new state variable, which we'll call "count"
    const [count, setCount] = useState(0);
    return (
        <div>
        Current Cart Count: {count}
            <div>
            <button onClick={() => setCount(count - 1)}>Add to cart</button>
            <button onClick={() => setCount(count + 1)}>Remove from cart</button>
            </div>
        </div>
    );
}

下面是该组件渲染后的样子。

Counter

通过点击 Add to cartRemove from cart 按钮,状态变量计数的值将发生变化,组件将以更新的状态值重新呈现。

useEffect Hook

如果你熟悉 React 类的生命周期方法(lifecycle methods),你可以把 useEffectHook 看作是componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期方法在一个函数中的结合。它让你在函数组件(functional components)中复制 React 的生命周期方法。

useEffect Hook 让你在函数组件(functional components)中执行副作用(side effects)。副作用是可以与组件的主要操作一起运行的动作,如外部 API 交互、修改 state 变量(state variables)和数据获取。

useEffect hook 接收两个参数:

  • 一个包含要运行的代码的函数
  • 一个包含组件范围(props、context 和 state variables)数值列表的数组,被称为依赖数组,它告诉 Hook 在每次更新其数值时都要运行。如果不提供,Hook 将在每次渲染后运行。

下面是一个使用 Hook 的例子:

import { useState, useEffect } from "react";
function Counter() {
    // Declare state variables
    const [count, setCount] = useState(0);
    const [product, setProduct] = useState("Eggs");
    useEffect(() => {
     console.log(`${product} will rule the world!`);
    });
    return (
        <div>
        Current {product}'s count: {count}
            <div>
                <button onClick={() => setCount(count + 1)}>Add to cart</button>
                <button onClick={() => setCount(count - 1)}>Remove from cart</button>
                Change Product:{" "}
                <input type="text" onChange={(e) => setProduct(e.target.value)} />
            </div>
        </div>
    );
}

在这个例子中,该效果将在每次状态更新后运行。

Effect-default

如何有条件地启动一个 Effect

要想在某些值发生变化时才运行 Hook,可以将变量作为依赖关系传入数组:

useEffect(() => {
 console.log(`${product} will rule the world!`);
}, [product]); // Only re-run the effect if the value of product changes

有了这个变化,Hook 将只在第一次渲染时运行,并且当产品的价格发生变化时。

Effect-dependency-array

如何在第一次渲染时运行一次

如果你想让一个 Effect 在第一次渲染时只运行一次,比如在组件第一次渲染时进行 API 调用,你可以像这样传递一个空数组作为其依赖。

useEffect(() => {
 console.log("This runs once on first render");
}, []);

通过提供一个空数组,它告诉 Hook 听从零状态变化(zero state changes),所以它只运行一次。

useContext Hook

useContext Hook 与 React Context API 一起工作。它为你提供了一种方法,使整个应用程序中的所有组件都能访问特定的数据,无论它们的嵌套有多深。

React 有一个单向的数据流,数据只能从父代传递到子代。要把数据(如 state)从父组件传给子组件,你需要根据子组件的嵌套深度,把它作为一个 prop,通过不同的级别向下传递。

对于诸如用户的首选语言、主题或认证用户的属性等数据,必须手动地在组件树上传递它们是很乏味的。

React的Context API和 useContext Hook 使得在应用程序的所有组件中传递数据变得容易。

它接受一个用React.createContext创建的上下文对象,并像这样返回当前的上下文:

const value = useContext(SomeContext);

让我们来看看 Hook 是如何工作的一个例子:

首先,创建一个使用 Hook 的上下文(context)。例如,这里有一个 UserContext 来获取当前用户的值:

import React from "react";
// some mock context values
const users = [
{
    name: "Harry Potter",
    occupation: "Wizard",
},
{
    name: "Kent Clark",
    occupation: "Super hero",
},
];

export const UserContext = React.createContext(users);

每个上下文(context)都有一个包装器提供者(Provider wrapper),它允许其子组件订阅上下文(context)的变化,并通过一个值 prop 传递上下文的值。

如果提供者的值 prop(value prop)被更新,其子组件将以新的上下文值重新渲染。

function Users() {
return (
    <UserContext.Provider value={users}>
    <UserProfile />
    </UserContext.Provider>
);
}

在这个例子中,UserProfile 被作为上下文的接收组件( consuming component)。

import React, { useContext } from "react";
import { UserContext } from "./App";

export function UserProfile() {
    const users = useContext(UserContext);
    return (
        <div>
            {users.map((user) => (
            <li>
            I am {user.name} and I am a {user.occupation}!
            </li>
            ))}
        </div>
    );
}

这将显示当前用户的属性:

image

useReducer Hook

useReducer Hooks 是 useState Hooks 的一个替代品。不同的是,它允许更复杂的逻辑和涉及多个子值的状态更新。

useState'类似,useReducer'允许你创建类似状态(state-like)的变量,每当它们发生变化时,都会导致用户界面的更新。

这个 Hook 接受 2 个参数:一个 reducer 函数和一个初始状态(initial state)。

useReducer(reducer, initialState);

它返回一个由两个值组成的数组,这两个值可以被解构为状态的当前值和一个调用函数。

const [state, dispatch] = useReducer(reducer, initialState);

让我们了解一下它的参数和返回值:

  • state:这是传递给 Hook 的 initialState 的当前值。
  • reducer:reducer 是一个接受状态和动作的函数。基于这些参数,它决定了状态的值将如何变化。
  • dispatch:调用函数是我们将动作传递给 reducer 函数,它分配动作以用于更新状态。

通常情况下,我们通过 switch 语句遍历我们在应用程序中的动作类型,以确定状态值将如何变化。这就是 Hook 更新其状态值的方式。

function reducer(state, action) {
    switch (action.type) {
        case "CASE_1":
        return {
         updatedState,
        };
        case "CASE_2":
        return {
         updatedState,
        };
        default:
         return state;
    }
}

dispatch function 通常以下列格式派发一个对象:

dispatch({ type: "ACTION_TYPE", payload: optionalArguments });

其中 type 是动作的描述,payload 是你要传递给 reducer 的参数。

如何创建自定义 Hooks

自定义 Hook 的概念是通过利用已有的 React Hook,将常用的组件逻辑从 UI 中提取到 JavaScript 函数中。这可以帮助你防止代码重复,并让你在多个组件中使这些逻辑可以重复使用。

让我们看看一个自定义 Hook 的例子,它将从我们传递给它的任何有效的 API URL 返回一个响应。

//useFetch.js
import { useState, useEffect } from "react";

export function useFetch(url) {
 //values
    const [data, setData] = useState(null);
    const [error, setError] = useState("");
    useEffect(() => {
        fetch(url)
        .then(res => {
            if (!res.ok) {
            throw Error("something wrong, çould not connect to resource");
        }
        setData(res.json());
        })
        .then(() => {
         setError("");
        })
        .catch( error => {
            console.warn(`sorry an error occurred, due to ${error.message} `);
            setData(null);
            setError(error.message);
        });
    }, [url]);
    return [data, error];
}

现在,你可以在你的应用程序中的任何地方使用这种逻辑,只需导入该函数并将 API 路径作为参数传递,而不是从头开始编写。

结语

我希望你能看到 React Hooks 是多么有用。它们可以让你快速创建有效的组件,而不用担心类组件所带来的麻烦。

从让你专注于写你的主要代码到允许你创建你自己的自定义 Hooks......React Hooks 是如此的酷!我很高兴你能自己尝试它们。

如果你觉得这篇文章有帮助,请与你的朋友分享它。另外,欢迎在 Twitter 和我的博客上与我联系,我在那里分享了大量的免费教育文章和资源。

谢谢你阅读本文,并祝你编程愉快!