目录
- 场景
- 优化方案
- 示例
- 延伸例子:为什么这很重要?
- [常见的请求 hook 封装](#常见的请求 hook 封装)
场景
- 如果你写了一个自定义 Hook,比如
useMyHook()
,它暴露出某些值或函数给外部组件使用。 - 那么你要特别注意 这些值或函数是否在每次渲染时都保持稳定 ,否则使用这些值的组件会频繁地重新渲染,或者造成外部
useEffect
、useCallback
、useMemo
等 Hook 的依赖不准确,出现难以追踪的 bug。
优化方案
- 如果你的 Hook 返回的是回一个值,你应该使用
useMemo
或者useRef
将它缓存。 - 如果你的 Hook 返回的是一个回调函数(如
handleChange
、onClick
),你应该使用useCallback
将它缓存。
即,尽量使用 React 已提供的稳定引用,比如 useMemo
, useCallback
, useRef
, 而不是每次都创建新值。这样能我们提供的 Hook 更高效、更可控。
示例
tsx
function useCounter() {
const [count, setCount] = useState(0);
// ⚠️ 不推荐:每次调用 useCounter 都返回新函数
// const increment = () => setCount(count + 1);
// ✅ 推荐:使用 useCallback 缓存回调
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
return { count, increment };
}
延伸例子:为什么这很重要?
tsx
function useTodos() {
const [todos, setTodos] = useState([]);
// 每次渲染都返回新引用
const addTodo = (text) => setTodos([...todos, { text }]);
return { todos, addTodo };
}
如果外部组件用 useEffect(() => { ... }, [addTodo])
,那它就会在每次渲染时重新执行,因为 addTodo
函数每次都是新引用。
改进后👇:
tsx
const addTodo = useCallback(
(text) => setTodos(todos => [...todos, { text }]),
[]
);
常见的请求 hook 封装
优化前
typescript
const useLoadData = (fetchData) => {
const [result, setResult] = useState({
type: "pending",
});
const loadData = async () => {
setResult({ type: "loading" });
try {
const data = await fetchData();
setResult({ type: "loaded", data });
} catch (err) {
setResult({ type: "error", error: err });
}
};
return { result, loadData };
};
优化后
下面是一个更高性能、语义更清晰且适合共享的版本,优化点包括:
优化点一览
- 使用
useCallback
缓存loadData
,防止外部组件因引用变化而频繁 re-render。 - 支持可选的自动加载(
autoLoad
)或依赖加载(传入依赖数组)。 - 支持外部传入初始状态和重置逻辑。
- 使用
useRef
存储isMounted
防止组件卸载后状态更新(可选安全处理)。
优化后的 useLoadData
typescript
import { useCallback, useEffect, useRef, useState } from "react";
type LoadState<T> =
| { type: "pending" }
| { type: "loading" }
| { type: "loaded"; data: T }
| { type: "error"; error: unknown };
interface UseLoadDataOptions {
autoLoad?: boolean;
deps?: any[];
}
function useLoadData<T>(
fetchData: () => Promise<T>,
options: UseLoadDataOptions = {}
) {
const { autoLoad = false, deps = [] } = options;
const [result, setResult] = useState<LoadState<T>>({ type: "pending" });
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const loadData = useCallback(async () => {
setResult({ type: "loading" });
try {
const data = await fetchData();
if (isMounted.current) {
setResult({ type: "loaded", data });
}
} catch (err) {
if (isMounted.current) {
setResult({ type: "error", error: err });
}
}
}, [fetchData]);
// 自动加载逻辑
useEffect(() => {
if (autoLoad) {
loadData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return { result, loadData };
}
使用方式示例:
typescript
const fetchUsers = () => fetch("/api/users").then(res => res.json());
const { result, loadData } = useLoadData(fetchUsers, {
autoLoad: true,
deps: [], // 你可以传入 [userId] 等依赖
});
if (result.type === "loading") return <p>加载中...</p>;
if (result.type === "error") return <p>出错啦:{String(result.error)}</p>;
if (result.type === "loaded") return <UserList users={result.data} />;
优点回顾
优化点 | 好处 |
---|---|
useCallback 缓存 |
外部组件可稳定依赖 loadData ,避免不必要副作用 |
自动加载支持 | 更通用,可在多场景下使用 |
isMounted 防护 |
防止异步请求完成后组件已经卸载导致的警告或错误 |
泛型支持 T |
类型更友好,适用于各种返回值类型的数据加载 |