关于
React 钩子 是 React 中的新增功能,它使你无需编写类即可使用状态和其他 React 功能。以下提供了易于理解的代码示例,以帮助你了解钩子(hook
)如何工作,并激发你在下一个项目中利用它们。
useTheme
使用此钩子可以轻松地使用CSS 变量动态更改应用程序的外观。你只需传入一个包含你要更新的 CSS 变量的键/值对的对象,然后该钩子即可更新文档根元素中的每个变量。在无法内联定义样式(无伪类支持)并且样式排列过多而无法在样式表中包含每个主题的情况下(例如,允许用户自定义个人资料外观的网络应用程序),这很有用。值得注意的是,许多css-in-js
库都支持开箱即用的动态样式,但是尝试仅使用 CSS 变量和React Hook
来实现这一点很有趣。下面的示例故意非常简单,但是你可以想象主题对象被存储在状态中或从 API 中获取。
import { useLayoutEffect } from "react";
import "./styles.scss"; // -> https://codesandbox.io/s/15mko9187
// 使用
const theme = {
"button-padding": "16px",
"button-font-size": "14px",
"button-border-radius": "4px",
"button-border": "none",
"button-color": "#FFF",
"button-background": "#6772e5",
"button-hover-border": "none",
"button-hover-color": "#FFF",
};
function App() {
useTheme(theme);
return (
);
}
// 钩子
function useTheme(theme) {
useLayoutEffect(() => {
for (const key in theme) {
// 更新文档根元素中的css变量
document.documentElement.style.setProperty(`--${key}`, theme[key]);
}
}, [theme]);
}
useHistory
通过此钩子,可以非常轻松地向应用程序添加撤消/重做功能。我们的例子是一个简单的绘图应用程序。它生成一个块网格,允许你单击任何块以切换其颜色。使用useHistory
钩子,我们可以撤消,重做或清除对画布的所有更改。在我们的钩子中,我们使用useReducer
来存储状态而不是useState
,对于任何使用过redux
的人来说,它应该看起来都很熟悉(在官方文档中了解有关useReducer
的更多信息)。钩子代码是从出色的use-undo
库复制而来,做了一些微小的更改,因此,如果你想将其插入到项目中,还可以通过 npm 使用该库。
import { useReducer, useCallback } from "react";
// 使用
function App() {
const { state, set, undo, redo, clear, canUndo, canRedo } = useHistory({});
return (
Click squares to draw
{((blocks, i, len) => {
// 生成块网格
while (++i <= len) {
const index = i;
blocks.push(
set({ ...state, [index]: !state[index] })}
key={i}
/>
);
}
return blocks;
})([], 0, 625)}
);
}
// 我们传入useReducer的初始状态
const initialState = {
// 每次新状态时更新的先前状态值的数组
past: [],
// 当前状态值
present: null,
// 如果撤消将包含“未来”状态值(因此我们可以重做)
future: [],
};
// 我们的reducer函数根据动作来处理状态变化
const reducer = (state, action) => {
const { past, present, future } = state;
switch (action.type) {
case "UNDO":
const previous = past[past.length - 1];
const newPast = past.slice(0, past.length - 1);
return {
past: newPast,
present: previous,
future: [present, ...future],
};
case "REDO":
const next = future[0];
const newFuture = future.slice(1);
return {
past: [...past, present],
present: next,
future: newFuture,
};
case "SET":
const { newPresent } = action;
if (newPresent === present) {
return state;
}
return {
past: [...past, present],
present: newPresent,
future: [],
};
case "CLEAR":
const { initialPresent } = action;
return {
...initialState,
present: initialPresent,
};
}
};
// 钩子
const useHistory = (initialPresent) => {
const [state, dispatch] = useReducer(reducer, {
...initialState,
present: initialPresent,
});
const canUndo = state.past.length !== 0;
const canRedo = state.future.length !== 0;
// 设置我们的回调函数
// 我们使用useCallback进行记录,以防止不必要的重新渲染
const undo = useCallback(() => {
if (canUndo) {
dispatch({ type: "UNDO" });
}
}, [canUndo, dispatch]);
const redo = useCallback(() => {
if (canRedo) {
dispatch({ type: "REDO" });
}
}, [canRedo, dispatch]);
const set = useCallback(
(newPresent) => dispatch({ type: "SET", newPresent }),
[dispatch]
);
const clear = useCallback(() => dispatch({ type: "CLEAR", initialPresent }), [
dispatch,
]);
// 如果需要,我们还可以返回过去和将来的状态
return { state: state.present, set, undo, redo, clear, canUndo, canRedo };
};
useScript
该钩子使动态加载外部脚本以及知道何时加载外部脚本变得非常容易。当你需要与第三方库(Stripe,Google Analytics
等)进行交互并且你希望在需要时加载脚本而不是将其包含在每个页面请求的文档头中时,此功能很有用。 在下面的示例中,我们等待脚本成功加载后再调用脚本中声明的函数。如果你有兴趣查看将其实现为高阶组件的形式,请查看react-script-loader-hoc
的源码。我个人觉得它像钩子一样可读。另一个优点是,与HOC
实现不同,因为多次调用同一钩子以加载多个不同的脚本非常容易,所以我们可以跳过添加对传递多个 src 字符串的支持。
import { useState, useEffect } from "react";
// 使用
function App() {
const [loaded, error] = useScript(
"https://pm28k14qlj.codesandbox.io/test-external-script.js"
);
return (
Script loaded: {loaded.toString()}
{loaded && !error && (
Script function call response: {TEST_SCRIPT.start()}
)}
);
}
// 钩子
let cachedScripts = [];
function useScript(src) {
// 跟踪脚本加载和错误状态
const [state, setState] = useState({
loaded: false,
error: false,
});
useEffect(() => {
// 如果cachedScripts数组已经包含src,则意味着这个钩子的...已经加载了此脚本,因此无需再次加载。
if (cachedScripts.includes(src)) {
setState({
loaded: true,
error: false,
});
} else {
cachedScripts.push(src);
// 创建 script 标签
let script = document.createElement("script");
script.src = src;
script.async = true;
// 用于加载和错误的脚本事件侦听器回调
const onScriptLoad = () => {
setState({
loaded: true,
error: false,
});
};
const onScriptError = () => {
// 从cachedScripts中删除,我们可以尝试再次加载
const index = cachedScripts.indexOf(src);
if (index >= 0) cachedScripts.splice(index, 1);
script.remove();
setState({
loaded: true,
error: true,
});
};
script.addEventListener("load", onScriptLoad);
script.addEventListener("error", onScriptError);
// 将脚本添加到 document body
document.body.appendChild(script);
// 清除事件监听器
return () => {
script.removeEventListener("load", onScriptLoad);
script.removeEventListener("error", onScriptError);
};
}
}, [src]);
return [state.loaded, state.error];
}
useKeyPress
该钩子可以轻松检测用户何时按下键盘上的特定键。例子非常简单,代码如下。
import { useState, useEffect } from "react";
// 使用
function App() {
//为每个要监视的键调用钩子
// Call our hook for each key that we'd like to monitor
const happyPress = useKeyPress("h");
const sadPress = useKeyPress("s");
const robotPress = useKeyPress("r");
const foxPress = useKeyPress("f");
return (
h, s, r, f
{happyPress && ""}
{sadPress && ""}
{robotPress && ""}
{foxPress && ""}
);
}
// 钩子
function useKeyPress(targetKey) {
// 跟踪是否按下按键的状态
const [keyPressed, setKeyPressed] = useState(false);
// 如果按下的键是我们的目标键,则设置为true
function downHandler({ key }) {
if (key === targetKey) {
setKeyPressed(true);
}
}
// 如果释放键是我们的目标键,则设置为false
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
// 添加事件监听器
useEffect(() => {
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
// 清除事件监听器
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, []);
return keyPressed;
}
useMemo
React 具有一个称为useMemo
的内置钩子,该钩子可让你记住执行开销大的函数,从而避免在每个渲染器上调用它们。你只需传递一个函数和一组输入数组,useMemo
仅在其中一个输入更改时才重新计算存储的值。在下面的示例中,我们有一个称为computeLetterCount
的函数(出于演示目的,我们通过包含一个大而完全不必要的循环使其变慢了)。当前选择的单词发生更改时,你会注意到一个延迟,因为它必须在新单词上调用computeLetterCount
。我们还有一个单独的计数器,该计数器在每次单击增量按钮时都会增加。当该计数器增加时,你会注意到渲染之间的延迟为零。这是因为没有再次调用computeLetterCount
。输入字未更改,因此返回了缓存的值。
import { useState, useMemo } from "react";
// 使用
function App() {
const [count, setCount] = useState(0);
const [wordIndex, setWordIndex] = useState(0);
const words = ["hey", "this", "is", "cool"];
const word = words[wordIndex];
// 返回单词中的字母数
// 我们通过包含一个较大且完全不必要的循环来使其变慢
const computeLetterCount = (word) => {
let i = 0;
while (i < 1000000000) i++;
return word.length;
};
const letterCount = useMemo(() => computeLetterCount(word), [word]);
return (
Compute number of letters (slow )
"{word}" has {letterCount} letters
Increment a counter (fast ⚡️)
Counter: {count}
);
}
useDebounce
此钩子可让你消除任何快速变化的值。当在指定的时间段内未调用useDebounce
钩子时,去抖动的值将仅反映最新的值。 当与useEffect
结合使用时,就像我们在下面的例子中所做的那样,你可以轻松地确保诸如 API 调用之类的昂贵操作不会过于频繁地执行。下面的示例使你可以搜索Marvel Comic API
,并使用useDebounce
防止在每次击键时触发 API 调用。
import { useState, useEffect, useRef } from "react";
// 使用
function App() {
const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// API请求
useEffect(() => {
if (debouncedSearchTerm) {
setIsSearching(true);
searchCharacters(debouncedSearchTerm).then((results) => {
setIsSearching(false);
setResults(results);
});
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
setSearchTerm(e.target.value)}
/>
{isSearching && Searching ...}
{results.map((result) => (
{result.title}
))}
);
}
function searchCharacters(search) {
// return fetch(/* ... */)
}
// 钩子
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
useOnScreen
该钩子可让你轻松检测元素在屏幕上何时可见,以及指定在屏幕上可见之前应显示多少元素。当用户向下滚动到特定位置时,显示特定元素,非常适合延迟加载图像或触发动画。
补充知识点: IntersectionObserver
import { useState, useEffect, useRef } from "react";
// 用法
function App() {
const ref = useRef();
// 调用传入ref和root边距的钩子,大于300px时元素可见
const onScreen = useOnScreen(ref, "-300px");
return (
Scroll down to next section
{onScreen ? (
Hey I'm on the screen
) : (
Scroll down 300px from the top of this section
)}
);
}
// 钩子
function useOnScreen(ref, rootMargin = "0px") {
// 用于存储元素是否可见的状态
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
//当观察者回调触发时更新我们的状态
setIntersecting(entry.isIntersecting);
},
{
rootMargin,
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.unobserve(ref.current);
};
}, []);
return isIntersecting;
}
usePrevious
一个经常出现的问题是“使用钩子时,如何获取props
或state
的先前值?”。 使用 React 类组件,你可以拥有componentDidUpdate
方法,该方法接收先前的props
和state
作为参数,或者你可以更新实例变量(this.previous = value
)并在以后引用它以获得先前的值。那么,如何在没有生命周期方法或实例来存储值的功能组件内部执行此操作?我们可以创建一个自定义钩子,该钩子在内部使用useRef
钩子来存储先前的值。
import { useState, useEffect, useRef } from "react";
// 用法
function App() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
// 同时显示当前和先前的计数值
return (
Now: {count}, before: {prevCount}
);
}
// 钩子
function usePrevious(value) {
// ref对象是一个通用容器,其当前属性是可变的...
// ...并且可以保存任何值,类似于类的实例属性
const ref = useRef();
// 将当前值存储在ref中
useEffect(() => {
ref.current = value;
}, [value]);
// 返回上一个值(在上述useEffect中更新之前发生)
return ref.current;
}
useOnClickOutside
该钩子可让你检测指定元素之外的点击。在下面的示例中,当单击模态之外的任何元素时,我们将使用它来关闭模态。通过将此逻辑抽象为一个钩子,我们可以轻松地在需要这种功能的所有组件(下拉菜单,工具提示等)中使用它。
import { useState, useEffect, useRef } from "react";
// Usage
function App() {
// 创建一个引用,添加到要检测其外部点击的元素中
const ref = useRef();
const [isModalOpen, setModalOpen] = useState(false);
// 传入ref的调用钩子和调用外部单击的函数
useOnClickOutside(
ref,
useCallback(() => setModalOpen(false), [])
);
return (
{isModalOpen ? (
Hey, I'm a modal. Click anywhere outside of me to close.
) : (
)}
);
}
// 钩子
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
// 如果单击ref的元素或后代元素,则不执行任何操作
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
useAnimation
该钩子可让你使用缓动功能(线性,弹性等)平滑地为任何值设置动画。在该示例中,我们调用useAnimation
钩子三次,以三个不同的间隔将三个球动画显示在屏幕上。我们的useAnimation
钩子实际上并没有利用useState
或useEffect
本身,而是充当useAnimationTimer
钩子的包装。将计时器逻辑抽象到自己的钩子中可以使我们更好地读取代码,并可以在其他上下文中使用计时器逻辑。
import { useState, useEffect } from "react";
// 用法
function App() {
const animation1 = useAnimation("elastic", 600, 0);
const animation2 = useAnimation("elastic", 600, 150);
const animation3 = useAnimation("elastic", 600, 300);
return (
);
}
const Ball = ({ innerStyle }) => (
);
// 钩子
function useAnimation(easingName = "linear", duration = 500, delay = 0) {
// useAnimationTimer钩子在每个动画帧都调用useState
const elapsed = useAnimationTimer(duration, delay);
// 指定的持续时间,范围为0-1
const n = Math.min(1, elapsed / duration);
// 根据指定的缓动函数返回更改后的值
return easing[easingName](n);
}
// 一些缓动函数从以下位置复制:
// https://github.com/streamich/ts-easing/blob/master/src/index.ts
const easing = {
linear: (n) => n,
elastic: (n) =>
n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15),
inExpo: (n) => Math.pow(2, 10 * (n - 1)),
};
function useAnimationTimer(duration = 1000, delay = 0) {
const [elapsed, setTime] = useState(0);
useEffect(() => {
let animationFrame, timerStop, start;
// 在每个动画帧上执行的函数
function onFrame() {
setTime(Date.now() - start);
loop();
}
//在下一个动画帧上调用onFrame()
function loop() {
animationFrame = requestAnimationFrame(onFrame);
}
function onStart() {
// 设置超时时间以在持续时间过去后停止操作
timerStop = setTimeout(() => {
cancelAnimationFrame(animationFrame);
setTime(Date.now() - start);
}, duration);
// 开始循环
start = Date.now();
loop();
}
// 在指定的延迟后开始(默认为0)
const timerDelay = setTimeout(onStart, delay);
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
};
}, [duration, delay]);
return elapsed;
}
useWindowSize
场景:获取浏览器窗口的当前大小。该钩子返回一个包含窗口宽度和高度的对象。如果在服务器端执行(没有窗口对象),则 width 和 height 的值将不确定。
import { useState, useEffect } from "react";
// 用法
function App() {
const size = useWindowSize();
return (
{size.width}px / {size.height}px
);
}
// 钩子
function useWindowSize() {
const isClient = typeof window === "object";
function getSize() {
return {
width: isClient ? window.innerWidth : undefined,
height: isClient ? window.innerHeight : undefined,
};
}
const [windowSize, setWindowSize] = useState(getSize);
useEffect(() => {
if (!isClient) {
return false;
}
function handleResize() {
setWindowSize(getSize());
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
}
useHover
检测鼠标是否悬停在元素上。该钩子返回一个 ref 和一个布尔值,该布尔值指示当前是否已将具有该 ref 的元素悬停。只需将返回的 ref 添加到要监视其悬停状态的任何元素。
import { useRef, useState, useEffect } from "react";
// 用法
function App() {
const [hoverRef, isHovered] = useHover();
return {isHovered ? "" : "☹️"};
}
// 钩子
function useHover() {
const [value, setValue] = useState(false);
const ref = useRef(null);
const handleMouseOver = () => setValue(true);
const handleMouseOut = () => setValue(false);
useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener("mouseover", handleMouseOver);
node.addEventListener("mouseout", handleMouseOut);
return () => {
node.removeEventListener("mouseover", handleMouseOver);
node.removeEventListener("mouseout", handleMouseOut);
};
}
}, [ref.current]);
return [ref, value];
}
useLocalStorage
将状态同步到本地存储,以使其在页面刷新后保持不变。用法与 useState 相似,不同之处在于我们传入了本地存储键,以便我们可以在页面加载时默认为该值,而不是指定的初始值。
import { useState } from "react";
// 用法
function App() {
// 与useState相似,但第一个参数是本地存储中的值的键。
const [name, setName] = useLocalStorage("name", "Bob");
return (
setName(e.target.value)}
/>
);
}
// 钩子
function useLocalStorage(key, initialValue) {
// 存储我们的值,将初始状态函数传递给useState
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
// 允许value是一个函数,因此我们具有与useState相同的API
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
参考
Easy to understand React hook recipes by Gabe Ragland
React Hooks FAQ.