React Hooks 快速入门指南
这篇文章将重点介绍 React Hooks,特别是 useState、useEffect 和 useRef。示例是为了清晰起见而特意编写的,并未遵循所有典型的最佳实践(例如将表情符号包裹在span
元素中😉)。
React Hooks 🎣
React Hooks 允许我们使用函数组件来完成曾经只能在 Class 组件中实现的功能——创建、持久化以及共享状态和行为逻辑。此外,Hooks 还能让我们利用组件生命周期中的某些时刻。
☝ 严格来说,一些钩子提供了一种模拟生命周期方法的方法,并且不是 1:1 的交换。
🤔 什么是钩子?
在术语之下,甚至在 React 本身之下,钩子是一个 JavaScript 函数,它遵循语法和预期参数形式的预定义模式。
有几种钩子,每种都有各自的目的和缺陷 - 但所有钩子都遵循一些规则:
-
钩子只能从函数组件或自定义钩子调用(这是另一篇文章的广泛主题!)
-
为了使 React 能够正确管理使用 hooks 创建的状态,每次重新渲染时,它们的调用顺序必须相同。因此,所有 hooks 都必须在组件的顶层调用。
☝ 函数组件就是一个函数!顶层是第一步,我们可能会在其中声明一个变量或进行设置——在条件测试、循环或执行可能导致突变的操作之前,以及在底层返回之前。
在这篇文章中,我们将介绍您在野外最有可能遇到的 3 个钩子:useState,useEffect和useRef。
1️⃣ useState 钩子
在 JavaScript 中,Class 对象的构建方式使得其自身的许多实例之间可以很容易地共享行为和值,部分原因是this
- 它本身就是一个令人困惑且深奥的主题。
另一方面,函数是有作用域的。每次调用时都会转储并重新创建其局部变量。函数中没有prev
或this
,如果没有外部变量,就不可能持久化值。
函数组件和类组件遵循同样的理念,因此在引入钩子之前,函数组件通常被称为无状态this
组件。如果没有状态,或者没有外部存储,这些组件就只能显示无法更新的数据……于是,我们引入了名为 useState 的钩子。
可以预见的是,useState 利用了 React 的状态系统 - 为函数组件创建一个添加独立状态片段的地方,同时提供一种更新和共享它们的方法。
语法和用法
要使用任何钩子,我们直接从 React 按名称导入它:
// import
import React, { useState } from 'react';
const App = () => {
return (
<div>
<p>Give 🐒 some 🍌!</p>
<button> + 🍌</button>
</div>
);
};
export default App;
要创建一个新的状态变量,我们将调用 useState 函数并传递所需的initial value
useState 的唯一参数。
在 Class 组件中,状态以对象的形式维护,新的状态值也必须遵循该格式。 useState 创建的状态变量彼此完全独立,这意味着我们的状态变量intial value
可以是对象,也可以是数字、字符串、数组等等。
我们将创建一个带有数字的计数:
import React, { useState } from 'react';
const App = () => {
// invoke
useState(0);
return (
<div>
<p>Give 🐒 some 🍌!</p>
<button> + 🍌</button>
</div>
);
};
export default App;
useState 函数返回两个值:当前状态变量(已赋初始值)和一个用于更新该值的函数。为了获取它们,我们将使用数组解构。
☝ 我们可以随意命名这些值,但惯例是使用
varName
/setVarName
样式:
import React, { useState } from 'react';
const App = () => {
// destructure return
const [bananaCount, setBananaCount] = useState(0);
return (
<div>
<p>Give 🐒 some 🍌!</p>
<button> + 🍌</button>
</div>
);
};
export default App;
就这样,我们创建了一个将在渲染之间持久化的状态片段。如果需要另一个状态片段,我们可以轻松创建。useState 在函数组件中调用的次数没有硬性限制。此功能可以轻松分离关注点并减少命名冲突。
在组件内部我们可以直接调用和使用它们,不需要“ this.state
”:
import React, { useState } from 'react';
const App = () => {
const [bananaCount, setBananaCount] = useState(0);
const [appleCount, setAppleCount] = useState(0);
return (
<div>
<p>Give 🐒 some 🍌!</p>
<p>🍌 : {bananaCount} </p>
<p>🍎 : {appleCount} </p>
<button
onClick={() => setBananaCount(bananaCount + 1)}> + 🍌</button>
<button
onClick={() => setAppleCount(appleCount + 1)}> + 🍎</button>
</div>
);
};
export default App;
除了提供创建新状态变量的方法之外,useState 钩子还可以通过在调用 setter 函数并更改数据时触发重新渲染来进入组件的生命周期。
2️⃣ useEffect 钩子
我们关注组件生命周期中的一些关键时刻,通常是因为我们希望在这些时刻发生后执行一些操作。这些操作可能包括网络请求、打开或关闭事件监听器等等。
在类组件中,我们使用生命周期方法componentWillMount
、componentDidMount
和 来实现这一点componentWillUnmount
。在函数组件中,我们现在可以将所有这些行为封装在 useEffect 钩子中,并完成类似生命周期方法的操作。
☝ 我记得这个钩子是“使用副作用”,因为它允许我们利用某些时刻并在发生时引起副作用。
语法和用法
要使用,请从 React 导入:
// import
import React, { useEffect, useState } from 'react';
// hardcoded data
const data = ["Doug", "Marshall", "Peter"];
const App = () => {
const [coolDudes, setCoolDudes] = useState(data);
return (
<div>Top 🆒 dudes:
{coolDudes.map((dude) => (
<p>😎{dude}</p>
))}
</div>
);
};
export default App;
现在,这个组件正在渲染一个列表coolDudes
,但这些都是硬编码的值——如果coolDudes
排名是在数据库中实时维护的呢?这样,我们的组件就可以始终拥有最新的数据,而我们不必自己更新它。
在使用 hooks 之前,我们需要将组件转换为类,或者将所需的逻辑移到链的上层。使用 useEffect hooks,我们可以在函数组件内部完成此任务。
要使用它,我们需要提供两个参数。首先是一个回调函数——我们想要调用的“副作用”;其次是一个依赖项数组——告诉回调函数何时运行。
import React, { useEffect, useState } from 'react';
// axios fetching library added
import axios from 'axios';
const App = () => {
const [coolDudes, setCoolDudes] = useState(data);
// invoke hook
useEffect(() => {
axios.get('http://superCoolApi/coolDudes')
.then((response) => {
setCoolDudes(response.data)
});
}, []);
return (
<div>Top 🆒 dudes are:
{coolDudes.map((dude) => (
<p>😎{dude}</p>
))}
</div>
);
};
export default App;
👆 你经常会在 useEffect hook 中看到类似 API 调用的操作。阅读组件代码时,可以很容易地将 useEffect 理解为“渲染后”的操作。其中的代码对组件结构的影响应该不大。
需要注意的是,useEffect 的第一个参数可能不是异步的。这与 React 中所有 hooks 必须在每次重新渲染时以相同的顺序调用的规则相关。虽然回调函数本身可能不是异步的,但我们可以在其中执行异步操作。
上面的示例使用 Promise 来解析 API 调用,但async
也await
可以使用 JavaScript:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const App = () => {
const [coolDudes, setCoolDudes] = useState(data);
// async fetch
useEffect(() => {
const response = async () => {
const { coolDudes } = await axios.get('http://superCoolApi/coolDudes')
}
setCoolDudes(coolDudes.data);
});
}, []);
return (
<div>Top 🆒 dudes are:
{coolDudes.map((dude) => (
<p>😎{dude}</p>
))}
</div>
);
};
export default App;
依赖数组
在上面两个例子中,我们都将一个空数组作为 useEffect 函数的第二个参数传递。这个第二个参数称为依赖数组,它是告诉 React何时应该运行回调函数的关键。
通过使用空数组、具有一个或多个值(通常是 state 或 props)的数组,或者完全省略参数,我们可以配置 useEffect 钩子以在特定时间自动运行。
清理函数
广义上讲,useEffect 函数中执行的操作有两种:一种需要清理,一种不需要清理。到目前为止,我们只发起了一个网络请求,这个操作会被调用、返回、存储并遗忘。它不需要清理。
但是,让我们想象一下,一个带有 useEffect 钩子的搜索组件,它利用 JavaScriptsetTimeout()
方法等待用户停止输入后再执行操作。这是一种巧妙且常见的 API 请求限制模式。
让我们来看一个简单而又人为的例子:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const App = () => {
// init state
const [search, setSearch] = useState("first search term");
// search state shared with debouncedSearch state 👇
const [debouncedSearch, setDebouncedSearch] = useState(search);
const [results, setResults] = useState([]);
useEffect(() => {
const search = async () => {
const { data } = await axios.get('http://searchApi.org', {
// options object to attach URL params
// API call is completed with the DEBOUNCED SEARCH
// These change depending on the API schema
params: {
action: 'query',
search: debouncedSearch
},
});
setResults(data.query.search);
};
if (debouncedSearch) search();
}, [debouncedSearch]);
return (
<React.Fragment>
<form>
<label htmlFor="search">Search</label>
<input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value}
placeholder="Search..." />
</form>
<div>
{results.map(result) => (
return <div key={result.id}>
<p>{result.title}</p>
</div>
</React.Fragment>
);
};
export default App;
现在,此组件渲染了一个搜索栏和一个搜索结果标题列表。首次渲染时,useEffect 会被调用,执行 API 调用,并将initial value
传入的 传递给search
状态切片,然后连接到debouncedSearch
状态。
但是,如果用户输入新的搜索词,则不会发生任何事情。这是因为依赖项数组正在监视debouncedSearch
状态,并且在该状态更新之前不会再次触发。同时,元素通过其propinput
绑定到状态。search
value
我们将调用 useEffect 钩子的另一个实例来连接这两个独立的状态,并在其中设置一个计时器:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const App = () => {
const [search, setSearch] = useState("first search term");
const [debouncedSearch, setDebouncedSearch] = useState(search);
const [results, setResults] = useState([]);
useEffect(() => {
const search = async () => {
const { data } = await axios.get('http://searchApi.org', {
params: {
action: 'query',
search: debouncedSearch
}
});
setResults(data.query.search);
}
if (debouncedSearch) search();
}, [debouncedSearch]);
useEffect(() => {
// create a timer that must end before calling setDebouncedSearch
const timerId = setTimeout(() => {
setDebouncedSearch(search);
}, 1000);
// useEffect can return a cleanup function! 🧼
return () => {
// this anonymous function will cleanup the timer in the case that the user keeps typing
clearTimeout(timerId);
};
}, [search]);
return (
<React.Fragment>
<form>
<label htmlFor="search">Search</label>
<input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value}
placeholder="Search..." />
</form>
<div>
{results.map(result) => (
return <div key={result.id}>
<p>{result.title}</p>
</div>
</React.Fragment>
);
};
export default App;
第二个 useEffect hook 通过其依赖项数组连接到搜索输入,用于监视search
状态的变化。状态更新时,该 hook 将被调用,其回调函数将使用 JavaScriptsetTimeout()
方法实例化一个计时器。
如果我们不在这个副作用之后进行清理,而用户继续输入,就会遇到问题。多个计时器会被添加到堆栈中,所有计时器都要等待 1000 毫秒才能触发 API 调用。这将带来糟糕的用户体验,但返回可选的清理函数可以轻松避免这种情况。
该函数将在钩子再次执行之前运行,使其成为在使用该clearTimeout()
方法创建新计时器之前取消最后一个计时器的安全位置。
☝ 清理函数不一定是匿名箭头函数,尽管通常你会看到它是这样。
3️⃣ useRef Hook
useRef 钩子用于将引用直接附加到 DOM 节点,或者存储我们预期会更改但又不希望触发代价高昂的重新渲染的数据。 useRef 函数返回一个可变ref
对象,该对象具有一个名为 的属性current
。该属性将指向我们赋值ref
给 的任何对象。
为了了解 useRef 钩子如何执行有趣且有用的任务,让我们直接进入一个用例。
语法和用法
由于 useRef 钩子的设计初衷是完成一项非常具体的工作,因此它的使用频率不如前两个钩子。但它可以用来实现用户在现代应用中所期望的流畅的 UI 交互。
例如,当我们打开下拉菜单或切换某些 UI 元素的打开状态时,我们通常希望它在以下情况下再次关闭:🅰 我们选择其中一个包含的选项,或单击元素本身。🅱 我们单击文档中的任何其他位置。
在 React 出现之前,当 JQuery 更为流行时,这是通过添加事件监听器来实现的。在 React 中,我们仍然可以添加事件监听器——要么使用React 自带的事件监听器onClick
和事件处理程序,要么使用 JavaScript 的副作用方法(例如 useEffect hook)。onChange
addEventListener()
下面的示例组件正在渲染文章列表。当点击标题时,onArticleSelect
会调用 并activeIndex
重新分配 ,从而触发open
状态(在 map 语句中创建renderedArticles
)发生变化,并展开文章的详细信息。
import React, { useState, useEffect } from "react";
// mock data
const data = [
{
id: 1,
title: "...",
details:
"..."
},
{
id: 2,
title: "...",
details: "..."
}
];
export default function App() {
const [articles] = useState(data);
const [activeIndex, setActiveIndex] = useState(null);
// change handler passed to the article element
const onArticleSelect = (id) => {
if (id === activeIndex) setActiveIndex(null);
else setActiveIndex(id);
};
// maps return from articles state
const renderedArticles = articles.map((article) => {
// isolate open status by performing a check
const open = article.id === activeIndex;
return (
<article
key={article.id}
style={{ border: "1px solid gray" }}
onClick={() => onArticleSelect(article.id)}
className="article"
>
<h2>{article.title}</h2>
<div> {open ? <p>{article.details}</p> : null} </div>
</article>
);
});
return (
<div className="App">
<div className="header">
<h1>🔥Hot Off the Presses🔥</h1>
</div>
<section className="articles">{renderedArticles}</section>
</div>
);
}
该组件具备我们想要的一些功能。点击后文章会展开,但文章只有在以下情况下才会再次关闭:🅰 再次点击或 🅱 为activeIndex
state 分配了另一个文章 ID。
我们希望在此基础上再添加一层,让用户点击文档中的任何其他元素时文章也会关闭。在这个小示例中,这不太实用,但如果将此组件导入并与许多其他组件一起渲染,可能会大大提升 UI 的体验。
body
我们将使用 useEffect hook 在组件首次渲染时在元素上设置事件监听器。监听器将检测到点击事件,并activeIndex
在触发时将其重置为 null:
import React, { useState, useEffect } from "react";
const data = [
{
id: 1,
title: "...",
details:
"..."
},
{
id: 2,
title: "...",
details: "..."
}
];
export default function App() {
const [articles] = useState(data);
const [activeIndex, setActiveIndex] = useState(null);
// change handler passed to the article element
const onArticleSelect = (id) => {
if (id === activeIndex) setActiveIndex(null);
else setActiveIndex(id);
};
// turns on body event listener
useEffect(() => {
const onBodyClick = (e) => {
// reset the active index
setActiveIndex(null);
};
document.body.addEventListener("click", onBodyClick, { capture: true });
}, []);
const renderedArticles = articles.map((article) => {
const open = article.id === activeIndex;
return (
<article
key={article.id}
style={{ border: "1px solid gray" }}
onClick={() => onArticleSelect(article.id)}
className="article"
>
<h2>{article.title}</h2>
<div> {open ? <p>{article.details}</p> : null} </div>
</article>
);
});
return (
<div className="App">
<div className="header">
<h1>🔥Hot Off the Presses🔥</h1>
</div>
<section className="articles">{renderedArticles}</section>
</div>
);
}
乍一看,这似乎可以正常工作——但有一个问题。当再次点击标题时,它不再切换显示。这与一种称为事件冒泡的编程原则有关,而 React 事件系统正是基于此原则构建的。
简而言之,我们分配给body
和article
元素的点击事件会经历一个协调过程。在此过程中,事件会从最外层的父元素冒泡向上,并且绑定到 的事件addEventListener()
始终会在我们通过 React 的 prop 附加的事件监听器之前被调用onClick
。
当第二次单击标题时,useEffect 中的事件监听器会首先触发,将设置activeIndex
为 null,然后onClick
处理程序会立即触发,将设置activeIndex
回我们尝试转储的原始索引。
为了解决这个问题,我们需要一种方法来告诉 React 用户何时点击了article
元素内部,何时点击了其他任何地方。为此,我们将使用 useRef 函数。
从 React 导入钩子后,我们将ref
在组件的顶层将其实例化为空。
☝ 最佳实践是直接在顶层调用钩子,将赋值操作留到组件返回时再执行,或者通过副作用(例如 useEffect 钩子)来实现。React 函数组件的渲染阶段应该是“纯”的——这意味着它不应该产生副作用,而更新操作
ref
会产生副作用。
import React, { useState, useEffect, useRef } from "react";
const data = [
{
id: 1,
title: "...",
details:
"..."
},
{
id: 2,
title: "...",
details: "..."
}
];
export default function App() {
const [articles] = useState(data);
const [activeIndex, setActiveIndex] = useState(null);
const ref = useRef();
const onArticleSelect = (id) => {
if (id === activeIndex) setActiveIndex(null);
else setActiveIndex(id);
};
useEffect(() => {
const onBodyClick = (e) => {
// adds a check: did the event occur in the ref node?
if (ref.current.contains(e.target)) {
// if yes, return early
return;
}
setActiveIndex(null);
};
document.body.addEventListener("click", onBodyClick, { capture: true });
// removes the event listener, should articles unmount 🧼
return () => {
document.body.removeEventListener("click", onBodyClick, {
capture: true
});
};
}, []);
const renderedArticles = articles.map((article) => {
const open = article.id === activeIndex;
return (
<article
key={article.id}
style={{ border: "1px solid gray" }}
onClick={() => onArticleSelect(article.id)}
className="article"
>
<h2>{article.title}</h2>
<div> {open ? <p>{article.details}</p> : null} </div>
</article>
);
});
return (
<div className="App">
<div className="header">
<h1>🔥Hot Off the Presses🔥</h1>
</div>
<section ref={ref} className="articles">
{renderedArticles}
</section>
</div>
);
}
我们将附加ref
到元素的最父article
元素,在本例中,它是section
类名为“articles”的。
useEffect 钩子也进行了更新以执行检查 - 根据检查的结果,body
事件监听器要么提前返回,不执行任何功能并允许onClick
处理程序不受阻碍地完成其工作,要么将activeIndex
再次执行并重置。
☝ 该
contains()
方法适用于所有 DOM 元素 - 这正是ref.current
本例所指向的。
Hooks 的引入改变了 React 生态系统,让曾经无状态的函数组件承担起巨大的复杂性和功能性。虽然 Hooks 与 Class 组件的生命周期方法并非一一对应,但它使我们能够创建高度可复用、可测试、可维护的组件和状态片段。
这里介绍的钩子只是故事的一部分,完整列表可以在官方 React 文档中找到。
资源:
- 使用 React 进行高级 Web 开发- Mehul Mohan,pdf 📕
- 现代 React 与 Redux - Stephen Grider,udemy 🏛
- React useRef Hook - Ceci García García,medium.com
- 将数据存储在状态变量中还是类变量中- seanmcp.com
- 使用 React useRef Hook 的巧妙方法- Aleem Isiaka,Smashing Magazine