React Hooks 教程:初学者学习 Hooks 的实用指南

2025-06-07

React Hooks 教程:初学者学习 Hooks 的实用指南

您是否曾经发现自己将 React 组件从基于函数的组件转换为基于类的组件,仅仅是因为您想要管理状态和/或生命周期逻辑?

我听过很多次!

嗯,你并不孤单。现在,功能组件不仅仅是一个展示组件。

随着React Hooks的引入,您将能够使用状态并管理函数组件内部基于类的生命周期逻辑。

这样做的好处是,您可以编写更易读、更简洁、更清晰的代码。您还可以拥有一种创建组件的方法。

在本教程中,你将学习如何实际使用这些 React Hooks。我们将开发一个简单的项目,其中状态逻辑和生命周期方法由类组件管理。

我们现在的任务是将管理逻辑从类组件切换到基于函数的组件。这样,你不仅可以学习基础知识,还可以学习如何在实际项目中应用它。

在深入学习之前,请确保你已经熟悉 React。如果不熟悉,可以从这里开始

什么是 React Hooks?

React Hooks(自 React 16.8 版起在 React 中引入)是 JavaScript 函数,允许我们仅使用函数组件来构建 React 组件。

React 捆绑了一些 Hook,使我们能够管理类逻辑的大多数用例。它还允许我们在需要重用组件逻辑时创建自定义 Hook。

在这里,我们将探讨内置 Hooks 的常见用例。

首先,让我们准备好项目文件。

从 GitHub 拉取项目文件

提供了一个启动项目。请从你的终端运行以下命令来克隆它:

git clone https://github.com/Ibaslogic/react-hooks-starter-project

这将以项目文件夹的名称创建一个目录。在本例中为react-hooks-starter-project

引导项目文件和文件夹后,使用文本编辑器打开它。在这里,我将使用 VsCode。

接下来,切换到目录(cd react-hooks-starter-project)并运行:

npm install

这将在本地node_modules文件夹中安装所有必要的依赖项。

最后,通过运行以下命令启动开发服务器:

npm start

您应该在浏览器地址栏中看到此应用程序,网址为http://localhost:3000/

React Hook Starter

(要从头开始构建这个待办事项应用程序,请查看这篇文章《初学者 React 教程》。)

这款应用功能简洁明了。你只需添加、勾选和删除待办事项即可。此外,点击复选框或删除按钮时,系统会提醒你。

正如您所期望的,您应该知道构成此 UI 的文件位于该src文件夹中。

如果你查看src/components文件夹内部,你会发现有五个组件文件。它们都是基于类的。

现在,让我们使用 React Hooks 优化我们的代码。

我们将从仅管理状态逻辑(而不是生命周期逻辑)的组件开始。

那么让我们看一下src/components/InputTodo.js文件。

目前,它有一个state对象(我们为title属性分配一个默认的空字符串)和位于组件顶层的类方法。

让我们首先注释掉所有代码。

然后在顶部添加此起始代码以避免分页:

import React from "react"

const InputTodo = () => {
  return <div></div>
}

export default InputTodo

这是第一次转换。注意,我们现在使用的是函数而不是类。

使用 React Hooks useState

为了在函数组件中添加状态,React 为我们提供了一个名为的 Hook useState

state如果你重新访问类组件,则可以使用 访问对象中定义的数据this.state。它也可以使用this.setState方法进行更新。

现在,让我们在函数组件中复制它。

首先,像这样useState从模块导入 Hook :react

import React, { useState } from "react"

const InputTodo = () => {
  console.log(useState("hello"))
  return <div></div>
}

export default InputTodo

请注意,我们正在记录 Hook 以查看我们得到的回报。

保存文件并打开浏览器 DevTools 的控制台。

设置状态

如上所示,useStateHook 返回一个数组,该数组始终包含两个元素。第一个元素是传入的当前值(在本例中为hello),第二个元素是一个允许我们更新该值的函数。

我们可以使用 JavaScript 数组解构从数组中获取这些项目。

例如,

const [title, setTitle] = useState("hello")

在这里,我们声明了一个名为的状态变量title(它保存当前状态,即hello)和一个名为的函数setTitle来更新状态。

this.state.title这与this.setState我们的类组件类似。

与类组件不同,状态不必是对象。它可以包含数组、数字和字符串(如上所示)。

另外,请注意,您并不像类组件那样只能定义一个状态属性。在这里,您可以定义多个状态。

您将在本指南的后面看到它的工作原理。

但请记住,将相关数据放在一起是件好事。

现在您已经有了一些基本的了解,让我们来看看使用这些 Hook 的规则。

你需要记住的是,你只能在函数组件的顶层或自定义 Hook 中调用 Hook。不能在循环、条件或常规函数中调用。

这可确保所有组件逻辑对 React 可见。

回到我们的代码,让我们更新组件,以便您拥有:

import React, { useState } from "react"

const InputTodo = props => {
  const [title, setTitle] = useState("")

  const onChange = e => {
    setTitle(e.target.value)
  }

  const handleSubmit = e => {
    e.preventDefault()
    props.addTodoProps(title)
    setTitle("")
  }

  return (
    <form onSubmit={handleSubmit} className="form-container">
      <input
        type="text"
        className="input-text"
        placeholder="Add todo..."
        value={title}
        name="title"
        onChange={onChange}
      />
      <input type="submit" className="input-submit" value="Submit" />
    </form>
  )
}

export default InputTodo

保存文件。您应该会在前端看到输入字段。

测试一下,它应该可以完美运行。

代码中发生了什么?

如果你回顾一下类的版本,我们声明了一个state对象,并在其中分配了一个键值对。但现在,我们使用useStateReact Hook 来实现这一点。

这里,我们不是使用this.state来访问当前状态值,而是简单地使用变量title。同样,我们现在使用 返回的第二个元素来更新状态useState

onChange正如在和函数中所见handleSubmit,我们使用了setTitle而不是this.setState在类组件中使用的。

注意: this类组件中的关键字在函数组件中不存在。

onChange这也适用于类组件(和)中的方法handleSubmit。记住,我们不能在函数中使用类方法,但可以在函数中定义函数。

所以,我们这里所做的就是通过添加关键字将类方法转换为函数const。通过这个简单的更改,您可以在 JSX 中调用该函数,而无需使用this关键字。

另一个需要关注的地方是onChange方法。每当输入文本字段发生变化时,都会调用此方法。

如果你足够警惕,你可能会问自己,为什么我们没有像在类版本中那样在方法e.target.name中使用 。如果你从头开始学习这个 React 教程,你就会知道,这个目标允许我们根据具体情况,用一个方法/函数来管理多个输入字段。onChange

现在仔细阅读。

在我们的代码中,我们通过 为查询变量分配一个字符串useState。这是 Hook 最简单的用例。

使用此设置,您只能在函数调用中管理一个输入字段。如果您添加更多字段,则需要定义一个单独的useStateHook 和一个函数来管理它。

虽然这很好,但最好将相关数据分组。

就像类版本的代码一样,我们将以一种可以使用函数管理尽可能多的输入字段的方式编写代码。

让我们更新InputTodo组件,以便您拥有:

import React, { useState } from "react"

const InputTodo = props => {
  const [inputText, setInputText] = useState({
    title: "",
  })

  const onChange = e => {
    setInputText({
      ...inputText,
      [e.target.name]: e.target.value,
    })
  }

  const handleSubmit = e => {
    e.preventDefault()
    props.addTodoProps(inputText.title)
    setInputText({
      title: "",
    })
  }

  return (
    <form onSubmit={handleSubmit} className="form-container">
      <input
        type="text"
        className="input-text"
        placeholder="Add todo..."
        value={inputText.title}
        name="title"
        onChange={onChange}
      />
      <input type="submit" className="input-submit" value="Submit" />
    </form>
  )
}

export default InputTodo

保存您的文件并测试您的工作。

现在,您可以使用单个函数(在我们的例子中是 函数)管理应用中的任意多个输入字段onChange。您只需在 旁边添加另一个属性titleuseState然后将属性名称分配给元素name中的 prop input

那么,有什么变化呢?

首先,任何时候您将对象中的相关数据分组(如状态变量的情况)时inputText,Hook 返回的状态useState不会与传递给它的更新的状态合并。

这意味着它不会合并新旧状态,而是用当前状态覆盖整个状态。

解决方法是通过使用扩展运算符( 之前的三个点)传递整个状态来手动合并它们,inputText并覆盖其中的一部分。

如果您不习惯像这样对相关数据进行分组,那么您可以将它们拆分成不同的useState。但别忘了,您需要单独的函数来管理它们。

希望清楚吗?

现在您已经了解了如何使用 React 内置useStateHook 来管理函数组件中的状态,让我们看看如何在函数组件中复制生命周期逻辑。

使用 React Hooks useEffect

我们现在的重点是src/components/TodoContainer.js文件。该文件管理一个名为的生命周期方法componentDidmount()

让我们在一个函数组件中复制它的逻辑。我相信你可以将这个组件中的状态逻辑转换为使用useStateHook。

好吧,我们就从那开始吧。

正如预期的那样,注释掉该文件中的所有代码,并在顶部添加以下内容。

import React, { useState } from "react"
import TodosList from "./TodosList"
import Header from "./Header"
import InputTodo from "./InputTodo"

import axios from "axios"
import uuid from "uuid"

const TodoContainer = props => {
  const [todos, setTodos] = useState([])
  const [show, setShow] = useState(false)

  const handleChange = id => {
    setTodos(
      todos.map(todo => {
        if (todo.id === id) {
          todo.completed = !todo.completed
        }
        return todo
      })
    )
    setShow(!show)
  }

  const delTodo = id => {
    setTodos([
      ...todos.filter(todo => {
        return todo.id !== id
      }),
    ])
  }

  const addTodoItem = title => {
    const newTodo = {
      id: uuid.v4(),
      title: title,
      completed: false,
    }
    setTodos([...todos, newTodo])
  }

  return (
    <div className="container">
      <Header headerSpan={show} />
      <InputTodo addTodoProps={addTodoItem} />
      <TodosList
        todos={todos}
        handleChangeProps={handleChange}
        deleteTodoProps={delTodo}
      />
    </div>
  )
}

export default TodoContainer

保存您的文件并测试您的应用程序。

注意,我们目前还没有包含生命周期逻辑,因此还没有获取任何数据。我们稍后会处理这个问题。

那么到底发生了什么?

在代码中,我们首先为useState状态变量定义一个单独的 Hook 并为它们分配一个默认值。

现在,将整个代码与类版本的代码进行比较,您会注意到我们删除了所有出现的代码,this.state因为它不适用于函数组件。

同样,用于更新状态值的setTodos和函数取代了它们各自的setShowthis.setState

除此之外,

如果您查看我们的代码的类版本,我们正在使用生命周期方法GET中的 HTTP 方法获取默认的 todos 数据componentDidMount

但是在函数组件中,我们不能使用这种方法。相反,我们将使用另一个名为 的 Hook useEffect

顾名思义,它用于执行副作用。例如,我们通过 HTTP 请求获取的数据。

React 允许我们使用单个 Hook 来组合不同的生命周期逻辑。因此,你可以将 useEffect Hook 视为componentDidMountcomponentDidUpdate和 的componentWillUnmount组合。

不过,就像 Hook 一样useState,您也可以使用多个 HookuseEffect来分离不相关的逻辑。

让我们看看如何应用这个 Hook。

src/components/TodoContainer.js文件中,useEffectreact模块导入 Hook。导入代码如下:

import React, { useState, useEffect } from "react"

然后在语句上方添加此 Hookreturn并保存文件:

useEffect(() => {
  console.log("test run")
})

通过这个简单的添加,如果您重新加载前端,您应该会看到浏览器控制台中显示的日志消息。

这个 Hook 接收一个函数作为参数,以及一个可选数组(我暂时忽略了它)。函数定义了要运行的副作用(在我们的例子中是发起一个 HTTP 请求),可选数组则定义了何时重新运行该副作用。

现在,让我们更新这个 Hook 以包含我们的 HTTP 请求。

useEffect(() => {
  console.log("test run")
  axios
    .get("https://jsonplaceholder.typicode.com/todos?_limit=10")
    .then(response => setTodos(response.data))
})

如果你保存文件并再次查看控制台,你会看到日志不断增加。这表明 Hook 正在无限运行。

useEffect 钩子

发生什么事了?

componentDidMount与仅在第一次获取数据后运行的生命周期不同,useEffect默认情况下,Hook 不仅在第一次渲染后运行,而且在每次更新后运行 - 即当 prop 或状态发生变化时。

在我们的代码中,todos当从端点获取数据时,状态变量会被更新。从而导致无限循环。

发生这种情况是因为 Hook 组合了不同的生命周期逻辑。我们有责任将其控制到我们想要的逻辑。

我们如何才能控制它?

这就是可选依赖项数组的用武之地。

useEffect(() => {
  ...
}, []);

如果指定的值(传入)在重新渲染之间没有改变,这允许我们跳过应用效果。

如果您传递一个空数组,React 将只执行一次 Hook,因为没有数据发生变化。

仔细观察一下,我们发现,componentDidMount当数组为空时以及componentDidUpdate当它包含将触发重新渲染的变量时,情况就等同于这种情况。

更新 Hook 以包含可选数组:

useEffect(() => {
  console.log("test run")
  axios
    .get("https://jsonplaceholder.typicode.com/todos?_limit=10")
    .then(response => setTodos(response.data))
}, [])

保存文件并测试您的应用程序。

它应该能按预期工作。

接下来我们看看如何处理useEffect的逻辑componentDidUpdatecomponentWillUnmount

从 开始componentDidUpdate

请记住,当状态或属性发生变化时,组件会更新,从而触发重新渲染。

如果你看一下这个src/components/Header.js文件,就会发现我们使用了这个生命周期方法来在 prop 发生变化时更新 DOM。每次点击复选框时都会发生这种情况。

要使用 Hook 应用此逻辑,

让我们首先将组件转换为基于函数的组件。

import React from "react"

const Header = props => {
  const headerStyle = {
    padding: "20px 0",
    lineHeight: "2em",
  }
  return (
    <header style={headerStyle}>
      <h1 style={{ fontSize: "25px", marginBottom: "15px" }}>
        Simple Todo App <span id="inH1"></span>
      </h1>
      <p style={{ fontSize: "19px" }}>
        Please add to-dos item(s) through the input field
      </p>
    </header>
  )
}

export default Header

目前,我们还没有生命周期逻辑。

我们现在就这么做。

useEffect像这样从模块导入react

import React, { useEffect } from "react"

然后在你的组件中添加这个 Hook Header(在顶层):

useEffect(() => {
  var x = Math.floor(Math.random() * 256)
  var y = Math.floor(Math.random() * 256)
  var z = Math.floor(Math.random() * 256)
  var bgColor = "rgb(" + x + "," + y + "," + z + ")"

  document.getElementById("inH1").innerHTML = "clicked"
  document.getElementById("inH1").style.backgroundColor = bgColor
}, [props.headerSpan])

保存您的文件并检查您的申请。

哎呀!初始渲染时显示的标题文本是“clicked”,但复选框并没有被点击。

发生什么事了?

如前所述,Hook 不仅在组件首次渲染时运行,而且在每次更新时都会运行。因此,在初始渲染时会执行其中定义的操作 DOM 的调用。

第一次渲染时,它会检查依赖项中的更新以便随后运行。

请记住,只要您单击复选框,此依赖关系就会更新。

虽然这是使用 Hook 的生命周期逻辑的常见用例,但有时我们希望 Hook 仅在更新时以及用户操作后立即运行。在我们的例子中,每当用户点击复选框时,都会运行。

仅在更新时运行效果

prevProps如果您重新访问我们代码的类版本,我们会通过比较和当前 prop来检查更新(即是否单击了复选框) 。

使用 React Hooks,我们可以根据情况使用useRef()Hook 获取先前的 props 或状态。

例如,在useEffectHook 上方添加:

const isInitialMount = useRef(true)

然后,将isInitialMount变量打印到控制台。确保useRefreact模块导入。

import React, { useEffect, useRef } from "react";
const Header = props => {
  const headerStyle = {
    ...
  };
  const isInitialMount = useRef(true);
  console.log(isInitialMount);
  useEffect(() => {
    ...
  }, [props.headerSpan]);
  return (
    ...
  );
};
export default Header;

如果您保存文件并检查控制台,您应该会看到以下内容:

useref 初始挂载

HookuseRef返回一个包含该current属性的对象。该属性被赋值,其值等于我们传递给 Hook 的参数。

这很好,因为我们可以跟踪我们是在第一次渲染还是后续渲染。

接下来,让我们更新useEffectHook,以便您拥有:

import React, { useEffect, useRef } from "react";

const Header = props => {
  const headerStyle = {
    ...
  };

  const isInitialMount = useRef(true);

  console.log(isInitialMount);

  useEffect(() => {
    var x = Math.floor(Math.random() * 256);
    var y = Math.floor(Math.random() * 256);
    var z = Math.floor(Math.random() * 256);
    var bgColor = "rgb(" + x + "," + y + "," + z + ")";

    if (isInitialMount.current) {
      isInitialMount.current = false;
    } else {
      document.getElementById("inH1").innerHTML = "clicked";
      document.getElementById("inH1").style.backgroundColor = bgColor;
    }
  }, [props.headerSpan]);

  return (
    ...
  );
};

export default Header;

保存您的文件并测试您的应用程序。

useref 初始挂载

代码中发生了什么?

useEffectHook 中,我们检查的当前属性是否useReftrue

默认情况下,我们将值设置为 ,true以跟踪组件的安装情况。当组件安装完成后,我们会忽略任何操作,并立即将值设置为false

至此,我们知道可以做任何想做的事情了。在我们的例子中,我们可以在复选框被点击后立即执行 DOM 操作。

继续。

接下来是componentWillUnmount逻辑。

这里,我们的重点是src/components/TodoItem.js文件。

通常,我们会在 中进行清理(例如,取消网络请求、删除事件监听器)componentWillUnmount。这是因为它在组件卸载和销毁之前立即调用。

但在我们的应用程序中,当某个项目即将从待办事项列表中删除时,我们使用这个生命周期逻辑来触发警报。

现在,我们如何使用 Hooks 复制相同的逻辑?

虽然您知道useEffectHook 在每次渲染时都会运行(除非您控制它),但 React 允许我们在运行另一个循环之前以及在卸载组件之前清理上一次渲染的效果。

好吧,让我们看看实际效果。

正如预期的那样,我们将把TodoItem类组件转换为基于函数的组件。

这应该是直截了当的。

给你:

import React from "react"

const TodoItem = props => {
  const completedStyle = {
    fontStyle: "italic",
    color: "#d35e0f",
    opacity: 0.4,
    textDecoration: "line-through",
  }

  const { completed, id, title } = props.todo

  return (
    <li className="todo-item">
      <input
        type="checkbox"
        checked={completed}
        onChange={() => props.handleChangeProps(id)}
      />
      <button onClick={() => props.deleteTodoProps(id)}>Delete</button>
      <span style={completed ? completedStyle : null}>{title}</span>
    </li>
  )
}

export default TodoItem

保存文件。

现在让我们应用卸载逻辑。

TodoItem组件中,在语句上方添加以下代码return

useEffect(() => {
  return () => {
    alert("Item about to be deleted!")
  }
}, [])

保存您的文件并测试您的应用程序。

React Hooks 组件将卸载

代码非常简单。每当你在useEffectHook 中返回一个函数时,它将在 Hook 下次运行之前执行(以防触发重新运行),并且也会在组件卸载之前执行。

在我们的例子中,我们没有任何数组依赖。因此,该效果只会运行一次,并且该return函数将在组件即将卸载时调用。

此时,您可以完全控制要创建的组件类型。

现在,我们的 todos 应用的逻辑已在函数式组件中使用 React Hooks 进行管理。不过,我们文件中仍然有一个使用类构建的组件src/components/TodosList.js

需要注意的是,该组件没有状态或生命周期逻辑。这使得转换变得简单直接。

你能尝试一下吗?

好的!

这是转换。

import React from "react"
import TodoItem from "./TodoItem"

const TodosList = props => {
  return (
    <div>
      {props.todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          handleChangeProps={props.handleChangeProps}
          deleteTodoProps={props.deleteTodoProps}
        />
      ))}
    </div>
  )
}

export default TodosList

现在,我们有一个仅用函数组件编写的完整的 React 应用程序。

感谢 Hooks。

虽然您已经学到了很多东西并涵盖了 React Hooks 的大多数用例,但还有更多内容需要学习,例如创建自定义 Hook 以实现逻辑可重用性。

但这是一个很好的开始!你可以在新项目和现有项目中使用这些 Hook。

请注意,您不必重写现有的逻辑,但您可以开始将这些 Hooks 应用于新的更新。

就是这样。

如果您喜欢本教程,请随时分享。此外,如果您有任何疑问,我很乐意在评论区解答。

在 Twitter 上关注我@ibaslogic

文章来源:https://dev.to/ibaslogic/react-hooks-tutorial-the-practical-guide-to-learning-hooks-for-beginners-56kn
PREV
Web 共享 API 实践
NEXT
React Context API 简化 – 实用指南(更新)