关于
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 (
<div>
<button className="button">Button</button>
</div>
);
}
// 钩子
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 (
<div className="container">
<div className="controls">
<div className="title">👩🎨 Click squares to draw</div>
<button onClick={undo} disabled={!canUndo}>
Undo
</button>
<button onClick={redo} disabled={!canRedo}>
Redo
</button>
<button onClick={clear}>Clear</button>
</div>
<div className="grid">
{((blocks, i, len) => {
// 生成块网格
while (++i <= len) {
const index = i;
blocks.push(
<div
// 如果状态对象为true,则给块“ active”类
className={"block" + (state[index] ? " active" : "")}
// 切换被点击的块的布尔值并合并为当前状态
onClick={() => set({ ...state, [index]: !state[index] })}
key={i}
/>
);
}
return blocks;
})([], 0, 625)}
</div>
</div>
);
}
// 我们传入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 (
<div>
<div>
Script loaded: <b>{loaded.toString()}</b>
</div>
{loaded && !error && (
<div>
Script function call response: <b>{TEST_SCRIPT.start()}</b>
</div>
)}
</div>
);
}
// 钩子
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 (
<div>
<div>h, s, r, f</div>
<div>
{happyPress && "😊"}
{sadPress && "😢"}
{robotPress && "🤖"}
{foxPress && "🦊"}
</div>
</div>
);
}
// 钩子
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 (
<div style={{ padding: "15px" }}>
<h2>Compute number of letters (slow 🐌)</h2>
<p>
"{word}" has {letterCount} letters
</p>
<button
onClick={() => {
const next = wordIndex + 1 === words.length ? 0 : wordIndex + 1;
setWordIndex(next);
}}
>
Next word
</button>
<h2>Increment a counter (fast ⚡️)</h2>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
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 (
<div>
<input
placeholder="Search Marvel Comics"
onChange={(e) => setSearchTerm(e.target.value)}
/>
{isSearching && <div>Searching ...</div>}
{results.map((result) => (
<div key={result.id}>
<h4>{result.title}</h4>
<img
src={`${result.thumbnail.path}/portrait_incredible.${result.thumbnail.extension}`}
/>
</div>
))}
</div>
);
}
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 (
<div>
<div style={{ height: "100vh" }}>
<h1>Scroll down to next section 👇</h1>
</div>
<div
ref={ref}
style={{
height: "100vh",
backgroundColor: onScreen ? "#23cebd" : "#efefef",
}}
>
{onScreen ? (
<div>
<h1>Hey I'm on the screen</h1>
<img src="https://i.giphy.com/media/ASd0Ukj0y3qMM/giphy.gif" />
</div>
) : (
<h1>Scroll down 300px from the top of this section 👇</h1>
)}
</div>
</div>
);
}
// 钩子
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 (
<div>
<h1>
Now: {count}, before: {prevCount}
</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// 钩子
function usePrevious(value) {
// ref对象是一个通用容器,其当前属性是可变的...
// ...并且可以保存任何值,类似于类的实例属性
const ref = useRef();
// 将当前值存储在ref中
useEffect(() => {
ref.current = value;
}, [value]);
// 返回上一个值(在上述useEffect中更新之前发生)
return ref.current;
}```
## useOnClickOutside
该钩子可让你检测指定元素之外的点击。在下面的示例中,当单击模态之外的任何元素时,我们将使用它来关闭模态。通过将此逻辑抽象为一个钩子,我们可以轻松地在需要这种功能的所有组件(下拉菜单,工具提示等)中使用它。
```javascript
import { useState, useEffect, useRef } from "react";
// Usage
function App() {
// 创建一个引用,添加到要检测其外部点击的元素中
const ref = useRef();
const [isModalOpen, setModalOpen] = useState(false);
// 传入ref的调用钩子和调用外部单击的函数
useOnClickOutside(
ref,
useCallback(() => setModalOpen(false), [])
);
return (
<div>
{isModalOpen ? (
<div ref={ref}>
👋 Hey, I'm a modal. Click anywhere outside of me to close.
</div>
) : (
<button onClick={() => setModalOpen(true)}>Open Modal</button>
)}
</div>
);
}
// 钩子
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 (
<div style={{ display: "flex", justifyContent: "center" }}>
<Ball
innerStyle={{
marginTop: animation1 * 200 - 100,
}}
/>
<Ball
innerStyle={{
marginTop: animation2 * 200 - 100,
}}
/>
<Ball
innerStyle={{
marginTop: animation3 * 200 - 100,
}}
/>
</div>
);
}
const Ball = ({ innerStyle }) => (
<div
style={{
width: 100,
height: 100,
marginRight: "40px",
borderRadius: "50px",
backgroundColor: "#4dd5fa",
...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 (
<div>
{size.width}px / {size.height}px
</div>
);
}
// 钩子
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 <div ref={hoverRef}>{isHovered ? "😁" : "☹️"}</div>;
}
// 钩子
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 (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
// 钩子
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];
}
useMemoCompare
这个钩子为我们提供了对象的记忆值,但是我们并没有像使用useMemo一样传递依赖项数组,而是传递了一个自定义的比较函数compare,该函数同时获取前一个值和新值。compare函数可以比较嵌套属性,调用对象方法或确定相等性所需执行的其他任何操作。 如果compare函数返回 true,则该钩子返回旧的对象引用。
该钩子解决了这样的问题:你想向其他开发人员提供一个接口,并且要求他们每次在调用时传传递一一个值。这里讲得有点抽象,我们还是直接上代码吧。
import React, { useState, useEffect, useRef } from "react";
// 用法
function MyComponent({ obj }){
const [state, setState] = useState();
// ---------------------------------------------------------
// 接收一个value和一个比较函数
const theObj = useMemoCompare(obj, (prev) => prev && prev.id === obj.id);
// 但是,如果我们直接使用obj,每次渲染上都会生成新的obj,useEffect将在每个渲染上触发
// 更糟糕的是,如果useEffect中产生了theObj的状态的改变,则可能导致无限循环
// 这里我们希望在theObj发生变化时触发useEffect
// (effect runs -> state change causes rerender -> effect runs -> etc ...)
useEffect(() => {
return theObj.someMethod().then((value) => setState(value));
}, [theObj]);
// ----------------------------------------------------------
// 那么为什么不只将[obj.id]作为依赖数组传递呢?
useEffect(() => {
// 好吧,那么eslint-plug-hooks会报依赖项数组中不存在obj
// 通过使用上面的钩子,我们可以更明确地将相等性检查与业务逻辑分开
return obj.someMethod().then((value) => setState(value));
}, [obj.id]);
// ----------------------------------------------------------
return <div> ... </div>;
};
// 钩子
function useMemoCompare(value, compare) {
// 使用Ref存储上一个value值
const previousRef = useRef();
const previous = previousRef.current;
// 往比较函数传递旧值与新值
const isEqual = compare(previous, value);
// isEqual值为false则更新previous值为最新值value并返回最新值
useEffect(() => {
if (!isEqual) {
previousRef.current = value;
}
});
return isEqual ? previous : value;
};
useAsync
通常向用户展示任何异步请求的状态是一种好实践。这里给出两个示例,一个是从API提取数据并在呈现结果之前显示加载指示符。 另一个在使用表单的过程中,你想在submit进行时禁用提交按钮,然后在完成时显示成功或错误消息。
这里并不使用一堆 useState 调来跟踪异步函数的状态,你可以使用我们的自定义钩子,该钩子接收一个异步函数作为入参并返回我们需要正确处理的类似pendeing,value,error的状态以便更新我们的用户界面。正如你将在下面的代码中看到的那样,我们的钩子返回了一个同时支持立即执行和延迟执行的execute函数。
import React, { useState, useEffect, useCallback } from "react";
// 用法
function App() {
const { execute, pending, value, error } = useAsync(myFunction, false);
return (
<div>
{value && <div>{value}</div>}
{error && <div>{error}</div>}
<button onClick={execute} disabled={pending}>
{!pending ? "Click me" : "Loading..."}
</button>
</div>
);
};
// 使用一个异步函数来测试我们的钩子,模拟各一半的请求成功率与失败率
function myFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const rnd = Math.random() * 10;
rnd <= 5
? resolve("Submitted successfully 🙌")
: reject("Oh no there was an error 😞");
}, 2000);
});
};
// 钩子
function useAsync(asyncFunction, immediate = true) {
const [pending, setPending] = useState(false);
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
// useCallback确保不调用以下useEffect
// 在每个渲染器上,但且仅当asyncFunction更改时
// 执行函数包装asyncFunction和处理挂起,结果值和错误的状态的设置
const execute = useCallback(() => {
setPending(true);
setValue(null);
setError(null);
return asyncFunction()
.then((response) => setValue(response))
.catch((error) => setError(error))
.finally(() => setPending(false));
}, [asyncFunction]);
// 否则可以稍后调用execute,例如在onClick处理程序中
// 如果要立即将其调用,请调用execute
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, pending, value, error };
};
useAuth
一个非常常见的场景,有一堆组件,这些组件需要根据当前用户是否登录以及有时需要调用身份验证方法(例如登录,注销,sendPasswordResetEmail等)而呈现不同的组件。
这是useAuth钩子的理想用例,它可以使任何组件获取当前的身份验证状态,并在其更改时重新渲染。这里并不是让useAuth钩子的每个实例都获取当前用户信息,而是简单地调用useContext来获取组件树中更顶层的数据。真正的魔法发生在我们的组件和useProvideAuth钩子中,该钩子包装了我们所有的身份验证方法(在这种情况下,我们使用 Firebase,一个完整的普适性身份验证解决方案),然后使用React Context使当前auth对象可用于所有调用useAuth的子组件。
// 根组件
import React from "react";
import { ProvideAuth } from "./use-auth.js";
function App(props) {
return <ProvideAuth>{/* 路由组件 */}</ProvideAuth>;
};
// 任何需要验证状态的组件
import React from "react";
import { useAuth } from "./use-auth.js";
function Navbar(props) {
// 获取身份验证状态并在任何更改时重新渲染
const auth = useAuth();
return (
<NavbarContainer>
<Logo />
<Menu>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
{auth.user ? (
<Fragment>
<Link to="/account">Account ({auth.user.email})</Link>
<Button onClick={() => auth.signout()}>Signout</Button>
</Fragment>
) : (
<Link to="/signin">Signin</Link>
)}
</Menu>
</NavbarContainer>
);
}
// 钩子 (use-auth.js)
import React, { useState, useEffect, useContext, createContext } from "react";
import * as firebase from "firebase/app";
import "firebase/auth";
// 添加你的Firebase凭据
firebase.initializeApp({
apiKey: "",
authDomain: "",
projectId: "",
appID: "",
});
const authContext = createContext();
// 包装你的应用,使任何调用useAuth()的子组件都能使用auth对象
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// 子组件获取auth对象...并在更改时重新渲染。
export const useAuth = () => {
return useContext(authContext);
};
// Provider程序钩子,用于创建auth对象并处理状态
function useProvideAuth() {
const [user, setUser] = useState(null);
// 校验登录
const signin = (email, password) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
});
};
// 校验注册
const signup = (email, password) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
});
};
// 校验登出
const signout = () => {
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
});
};
// 校验邮件密码重置
const sendPasswordResetEmail = (email) => {
return firebase
.auth()
.sendPasswordResetEmail(email)
.then(() => {
return true;
});
};
// 校验密码重置
const confirmPasswordReset = (code, password) => {
return firebase
.auth()
.confirmPasswordReset(code, password)
.then(() => {
return true;
});
};
// 组件挂载完订阅用户
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
});
// 组件卸载时清除评阅
return () => unsubscribe();
}, []);
// 返回携带用户信息的user对象及验证方法
return {
user,
signin,
signup,
signout,
sendPasswordResetEmail,
confirmPasswordReset,
};
}
useRouter
如果你使用React Router,你可能已经注意到他们最近添加了许多有用的钩子,特别是useParams,useLocation,useHistory和useRouteMatch。但是让我们看看是否可以通过将它们包装到一个只暴露我们需要的数据和方法的useRouter钩子中来使其更简单。 在本例子中,我们展示了组合多个钩子并将它们的返回状态组合到一个对象中是多么容易。对于像React Router这样的库提供一系列低阶钩子是很有意义的,因为仅使用所需的钩子就可以最大程度地减少不必要的重新渲染。就是说,有时你希望获得更简单的开发人员体验,而自定义钩子则使之成为功能。
import {
useParams,
useLocation,
useHistory,
useRouteMatch,
} from "react-router-dom";
import queryString from "query-string";
// 用法
function MyComponent() {
const router = useRouter();
// 从查询字符串(?postId=123)或路由参数(/:postId)获取值
console.log(router.query.postId);
// 获取当前路径
console.log(router.pathname);
// 使用router.push()进行路由
return <button onClick={(e) => router.push("/about")}>About</button>;
}
// 钩子
export function useRouter() {
const params = useParams();
const location = useLocation();
const history = useHistory();
const match = useRouteMatch();
// useMemo以便仅在发生某些更改时才返回新对象
// 返回我们的自定义路由器对象
return useMemo(() => {
return {
// 为了方便起见,在顶层添加push(),replace(),路径名
push: history.push,
replace: history.replace,
pathname: location.pathname,
// 将参数和已解析的查询字符串合并为单个“查询”对象,以便它们可以互换使用
// 例如: /:topic?sort=popular -> { topic: "react", sort: "popular" }
query: {
...queryString.parse(location.search),
...params,
},
// 包含match,location,history,以便可以使用额外的React Router功能。
match,
location,
history,
};
}, [params, match, location, history]);
}
useRequireAuth
常见的一种需求是在用户注销后重定向用户并尝试查看应该要求其进行身份验证的页面。本示例说明了如何轻松地组合我们的useAuth和useRouter钩子来创建一个新的useRequireAuth钩子,该钩子可以做到这一点。当然,可以将此功能直接添加到我们的useAuth钩子中,但是随后我们需要使该钩子了解我们的路由器逻辑。利用钩子组合的功能,我们可以使其他两个钩子尽可能地简单,并且在需要重定向时仅使用新的useRequireAuth。
import Dashboard from "./Dashboard.js";
import Loading from "./Loading.js";
import { useRequireAuth } from "./use-require-auth.js";
function DashboardPage(props) {
const auth = useRequireAuth();
if (!auth) {
return <Loading />;
}
return <Dashboard auth={auth} />;
}
// 钩子 (use-require-auth.js)
import { useEffect } from "react";
import { useAuth } from "./use-auth.js";
import { useRouter } from "./use-router.js";
function useRequireAuth(redirectUrl = "/signup") {
const auth = useAuth();
const router = useRouter();
useEffect(() => {
if (auth.user === false) {
router.push(redirectUrl);
}
}, [auth, router]);
return auth;
}
useEventListener
如果你发现自己使用useEffect添加了许多事件侦听器,则可以考虑将逻辑移至自定义钩子。在下面的例子中,我们创建一个useEventListener钩子,该钩子用于检查是否支持addEventListener,添加事件侦听器以及清除时移除。
import { useState, useRef, useEffect, useCallback } from "react";
// 用法
function App() {
const [coords, setCoords] = useState({ x: 0, y: 0 });
const handler = useCallback(
({ clientX, clientY }) => {
setCoords({ x: clientX, y: clientY });
},
[setCoords]
);
// 使用我们的钩子添加事件侦听器
useEventListener("mousemove", handler);
return (
<h1>
The mouse position is ({coords.x}, {coords.y})
</h1>
);
}
// 钩子
function useEventListener(eventName, handler, element = window) {
// 创建引用存储handler
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
// 确保节点支持addEventListener
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// 创建事件侦听器,调用存储在ref中的处理函数
const eventListener = (event) => savedHandler.current(event);
// 添加事件侦听
element.addEventListener(eventName, eventListener);
// 清除侦听
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}
useWhyDidYouUpdate
通过此钩子,可以轻松查看是哪些prop的更改导致了组件重新渲染。如果一个函数的运行成本特别高,并且你知道在给定相同的prop的情况下它可以返回相同的结果,则可以使用React.memo高阶组件,就像下面示例中对Counter组件所做的那样。在这种情况下,如果仍然看到所谓的不必要的重新渲染,则可以放入useWhyDidYouUpdate钩子并检查控制台(console),以查看哪些props在渲染之间进行了更改,并查看其上一个值/当前值。
import { useState, useEffect, useRef } from "react";
// 在使用React.memo后如果仍看到不必要的渲染,则可以使用useWhyDidYouUpdate并检查控制台以查看发生了什么。
const Counter = React.memo((props) => {
useWhyDidYouUpdate("Counter", props);
return <div style={props.style}>{props.count}</div>;
});
function App() {
const [count, setCount] = useState(0);
const [userId, setUserId] = useState(0);
const counterStyle = {
fontSize: "3rem",
color: "red",
};
return (
<div>
<div className="counter">
<Counter count={count} style={counterStyle} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
<div className="user">
<img src={`http://i.pravatar.cc/80?img=${userId}`} />
<button onClick={() => setUserId(userId + 1)}>Switch User</button>
</div>
</div>
);
}
// 钩子
function useWhyDidYouUpdate(name, props) {
// 定义一个可变的ref对象,可以在其中存储props以便下次运行此钩子时进行比较。
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
// 拿到上一个和当前props获取所有键
const allKeys = Object.keys({ ...previousProps.current, ...props });
// 使用此对象来跟踪更改的props
const changesObj = {};
allKeys.forEach((key) => {
if (previousProps.current[key] !== props[key]) {
changesObj[key] = {
from: previousProps.current[key],
to: props[key],
};
}
});
if (Object.keys(changesObj).length) {
console.log("[why-did-you-update]", name, changesObj);
}
}
// 最后用当前props更新以前的props
previousProps.current = props;
});
}
useMedia
该钩子使在组件逻辑中利用媒体查询变得非常容易。在下面的示例中,我们根据与当前屏幕宽度匹配的媒体查询来呈现不同数量的列,然后以限制列高差的方式在各列之间分配图像(我们不希望某一列比其他列更长 )。
你可以创建一个直接测量屏幕宽度的钩子,而不使用媒体查询。这种方法使在 JS 和样式表之间共享媒体查询变得容易。
import { useState, useEffect } from "react";
function App() {
const columnCount = useMedia(
// 媒体查询
["(min-width: 1500px)", "(min-width: 1000px)", "(min-width: 600px)"],
// 列数(根据数组索引取值,与上述媒体查询有关)
[5, 4, 3],
//默认列数
2
);
// 以列高值创建数组(从0开始)
let columnHeights = new Array(columnCount).fill(0);
// 创建二组数组,容纳每一列项目
let columns = new Array(columnCount).fill().map(() => []);
data.forEach((item) => {
const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
columns[shortColumnIndex].push(item);
columnHeights[shortColumnIndex] += item.height;
});
return (
<div className="App">
<div className="columns is-mobile">
{columns.map((column) => (
<div className="column">
{column.map((item) => (
<div
className="image-container"
style={{
// Size image container to aspect ratio of image
paddingTop: (item.height / item.width) * 100 + "%",
}}
>
<img src={item.image} alt="" />
</div>
))}
</div>
))}
</div>
</div>
);
}
// 钩子
function useMedia(queries, values, defaultValue) {
const mediaQueryLists = queries.map((q) => window.matchMedia(q));
// 根据匹配的媒体查询拿到value
const getValue = () => {
// 获取匹配的第一个媒体查询的索引
const index = mediaQueryLists.findIndex((mql) => mql.matches);
// 返回相关值,如果没有则返回默认值
return typeof values[index] !== "undefined" ? values[index] : defaultValue;
};
const [value, setValue] = useState(getValue);
useEffect(() => {
// setValue可以接收一个函数来处理值更新
const handler = () => setValue(getValue);
// 为每个媒体查询设置一个侦听器,并以上述处理程序作为回调。
mediaQueryLists.forEach((mql) => mql.addListener(handler));
return () => mediaQueryLists.forEach((mql) => mql.removeListener(handler));
}, []);
return value;
}
useLockBodyScroll
有时,当特定组件绝对位于页面上时,你可能想阻止用户滚动页面的主体(典型的如模态滑动穿透)。看到背景内容在模态下滚动可能会造成混淆,特别是如果你打算滚动模态内的某个区域。这个钩子解决了这样的问题,只需在任何组件中调用useLockBodyScroll钩子,主体滚动将被锁定,直到该组件卸载为止。
import { useState, useLayoutEffect } from "react";
// 用法
function App() {
const [modalOpen, setModalOpen] = useState(false);
return (
<div>
<button onClick={() => setModalOpen(true)}>Show Modal</button>
<Content />
{modalOpen && (
<Modal
title="Try scrolling"
content="I bet you you can't! Muahahaha 😈"
onClose={() => setModalOpen(false)}
/>
)}
</div>
);
}
function Modal({ title, content, onClose }) {
// 调用钩子以锁定主体滚动
useLockBodyScroll();
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal">
<h2>{title}</h2>
<p>{content}</p>
</div>
</div>
);
}
// 钩子
function useLockBodyScroll() {
useLayoutEffect(() => {
// 拿到原始body overflow的值
const originalStyle = window.getComputedStyle(document.body).overflow;
// 防止body在模态显示的情况下滚动
document.body.style.overflow = "hidden";
// 模态组件卸载时重置overflow
return () => (document.body.style.overflow = originalStyle);
}, []);
}
useInterval
相比setInterval, useInterval就一个用处,delay可以动态修改
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}