跳到主要内容

React Hooks 速查表:17 个内置 hook + 10 个自定义 hook 范式,带真实例子和坑

React Hooks 速查表,17 个内置 hook (useState / useEffect / useMemo / useTransition / useFormStatus...) 含真实例子和常见坑。

  • 本地处理
  • 分类 开发运维
  • 适合 格式化、校验、压缩或检查和代码相关的文本。
46 个 hook
内置 (18)

useState

内置
函数签名const [state, setState] = useState<T>(initial: T | (() => T))

最常用的状态 hook。返回 [当前值, setter] 元组。setter 在多次渲染之间引用稳定,调用它会触发用新值重新渲染。

何时用

组件内任何"改变要触发重渲染"的值:表单输入、开关、计数器、拉到的数据、UI 状态。

何时不用

从已有 state 派生出来的值(直接算或用 useMemo 即可)。不需要触发重渲染的引用值(用 useRef)。多种状态转换交织的复杂状态(用 useReducer)。

常见坑: 闭包陷阱:连续两次 setState(count + 1) 只会 +1 一次,因为两次读到的 count 是同一个值。下一个值依赖上一个时,必须用函数式写法 setState(c => c + 1)。

例子
const [count, setCount] = useState(0);
// functional form is mandatory when next depends on prev
const inc = () => setCount(c => c + 1);
// lazy initializer — fn runs once, not on every render
const [grid, setGrid] = useState(() => buildExpensiveGrid(1000));

useReducer

内置
函数签名const [state, dispatch] = useReducer<R>(reducer, initialArg, init?)

适合"多种动作切换同一个状态"的写法。reducer 是 (state, action) => state 的函数。dispatch 引用稳定,传给子组件不会破坏 memo。

何时用

5 个以上字段的表单。idle/loading/success/error 多状态的异步流程。任何在一个 handler 里要连写多个相关 setState 的场景。

何时不用

单个布尔或计数器(useState 更短)。只在一个组件里、状态转换不超过两种的(useState 更少代码)。

常见坑: reducer 必须是纯函数,里面不能有 fetch / setTimeout / 操作 DOM。副作用要放到 useEffect 或调 dispatch 的那个 handler 里。

例子
type Action = { type: 'inc' } | { type: 'reset' };
function reducer(state: number, action: Action) {
  switch (action.type) {
    case 'inc': return state + 1;
    case 'reset': return 0;
  }
}
const [count, dispatch] = useReducer(reducer, 0);
// async flow modeled as a state machine
type S = { status: 'idle' | 'loading' | 'ok' | 'err'; data?: User };
type A = { type: 'start' } | { type: 'ok'; data: User } | { type: 'err' };

useEffect

内置
函数签名useEffect(effect: () => (void | (() => void)), deps?: any[])

在浏览器绘制 之后 跑副作用。可选的清理函数会在下一次 effect 之前以及卸载时跑。依赖数组控制 effect 何时重新触发。

何时用

订阅外部系统(WebSocket、事件监听、observer)。同步非 React 状态(localStorage、document.title)。任何要在 DOM 更新 之后 响应 props/state 变化的事。

何时不用

从 props 派生 state(直接算就行,不用 effect)。每次都跑、不带清理的(根本不需要 effect,函数体里写就行)。props 变了想重置 state(用 `key` 属性或渲染期对比上次值)。

常见坑: 依赖数组漏了 = 闭包陷阱:effect 抓的是它被 定义 时那一渲染的值。一定要打开 `eslint-plugin-react-hooks` 的 exhaustive-deps,把读到的每个值都列进去,或者把值挪到 ref 里、挪到组件外。

例子
useEffect(() => {
  const id = setInterval(() => setTick(t => t + 1), 1000);
  return () => clearInterval(id);   // cleanup is mandatory for timers
}, []);
// subscribe + cleanup pattern
useEffect(() => {
  const onResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, []);
// data fetching with abort to avoid setState-after-unmount warning
useEffect(() => {
  const ctrl = new AbortController();
  fetch(`/api/user/${id}`, { signal: ctrl.signal })
    .then(r => r.json()).then(setUser).catch(() => {});
  return () => ctrl.abort();
}, [id]);

useLayoutEffect

内置
函数签名useLayoutEffect(effect: () => (void | (() => void)), deps?: any[])

签名和 useEffect 一样,但在 DOM 更新之后、浏览器绘制 之前 同步触发。用来量尺寸或改布局可以避免视觉抖动。

何时用

量 DOM 尺寸,然后在同一帧里定位另一个元素。读滚动位置。同步修正布局,防止用户看到错的中间态闪一下。

何时不用

不需要"同一帧视觉一致"的事都不要用。重活(会卡住绘制、掉帧)。SSR:useLayoutEffect 在服务器不跑,React 会警告。

常见坑: 它会阻塞绘制。里面做重活就卡顿。SSR 时收到 "useLayoutEffect does nothing on the server" 警告,要么换 useEffect,要么把它隔离起来。

例子
useLayoutEffect(() => {
  const { height } = ref.current!.getBoundingClientRect();
  setRowHeight(height);   // setState is safe — it batches before paint
}, []);

useInsertionEffect

内置
函数签名useInsertionEffect(effect: () => (void | (() => void)), deps?: any[])

面向 CSS-in-JS 库的小众 hook。在任何 DOM 变更 之前 同步触发,让库可以注入 <style> 标签而不引发布局抖动。

何时用

几乎不在业务代码里用。设计目标是 styled-components / Emotion / Stitches 这类库,在 React commit 前插入生成的样式表。

何时不用

业务代码几乎一定该用 useEffect 或 useLayoutEffect。在这里读布局是错的,DOM 还没变更。这阶段 ref 也还没挂上。

常见坑: ref 还没挂,而且这里不能 setState,React 官方文档明说。当它是库专用 hook 就行。

例子
// styled-components-style use case
useInsertionEffect(() => {
  const tag = document.createElement('style');
  tag.textContent = generatedCss;
  document.head.appendChild(tag);
  return () => tag.remove();
}, [generatedCss]);

useMemo

内置
函数签名const value = useMemo<T>(() => compute(), deps)

在多次渲染之间缓存一个昂贵计算的返回值。只有依赖数组里的值变化时,函数才重新跑。

何时用

昂贵的纯计算(长列表排序 + 过滤、复杂解析)。给 memo 子组件或者其他 hook 的依赖数组,提供引用稳定的对象/数组。

何时不用

便宜的计算:useMemo 自己也有开销。只是"加上心里踏实"的场合都别加。给所有东西套一层是公认的反模式,只让 bundle 变大、性能更差。

常见坑: useMemo 是"建议",不是"保证",React 可以为释放内存而丢弃缓存。绝不能把"只能跑一次"的逻辑放进去;要保证只跑一次用 useEffect([]) 或 ref。

例子
const visible = useMemo(
  () => items.filter(i => i.name.includes(query)).sort(byDate),
  [items, query],
);
// stabilize an object so memoized child does not re-render
const style = useMemo(() => ({ color, fontSize }), [color, fontSize]);

useCallback

内置
函数签名const fn = useCallback<T>(callback, deps)

在多次渲染之间缓存函数引用,让 React.memo 的子组件或下游 hook 拿到稳定的回调。等价于 useMemo(() => fn, deps)。

何时用

把 handler 传给 React.memo / PureComponent 的子组件。把函数放进另一个 hook 的依赖数组。除此之外,别用。

何时不用

没有 memo 的子组件用了没用(反正都会重渲染)。给所有 handler 都套一层和给所有值套 useMemo 一样,是反模式。

常见坑: 和 useEffect 一样的闭包陷阱:漏依赖会让缓存的函数读到 旧 值。必须开 exhaustive-deps lint。

例子
const onSelect = useCallback(
  (id: string) => setSelected(s => ({ ...s, [id]: true })),
  [],   // setSelected from useState is already stable
);

useRef

内置
函数签名const ref = useRef<T>(initial: T): { current: T }

返回一个可变的 .current 对象,跨渲染保留但 不会 触发重渲染。两种用法:(1) 通过 ref 属性拿 DOM 节点,(2) 当作类的实例变量存可变值。

何时用

读或聚焦 DOM 节点。存定时器 id、上一次的值、不能触发重渲染的标志位。存可变缓存。

何时不用

UI 需要响应的状态:用 useState。ref 不参与协调,改 .current 不会触发重渲染。

常见坑: 渲染期读 ref.current 很脆,DOM ref 在首次渲染时是 null。要在 useEffect / useLayoutEffect 或事件 handler 里读,别在渲染函数里读。

例子
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />;
// instance value pattern — no re-render on change
const lastSeenRef = useRef(0);
function onScroll() { lastSeenRef.current = window.scrollY; }

useImperativeHandle

内置
函数签名useImperativeHandle<T, R extends T>(ref, () => R, deps?)

自定义通过 ref 暴露给父组件的值。要和 forwardRef 配合。用来暴露窄接口(如 { focus, scrollIntoView }),不直接给原始 DOM。

何时用

可复用的输入组件,向父级暴露 focus() 和 clear()。模态框暴露 open()/close()。任何被封装的组件,父级需要少量命令式 API。

何时不用

能用 props 声明式表达的就别用,优先 props。要暴露整个 DOM 节点的话,直接 forwardRef 转发就够了。

常见坑: 不套 forwardRef,函数组件上的 ref 等于无效。漏写依赖数组,每次渲染都会重建那个命令式对象。

例子
const Input = forwardRef<{ focus: () => void }, Props>((props, ref) => {
  const inner = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => ({
    focus: () => inner.current?.focus(),
  }), []);
  return <input ref={inner} {...props} />;
});

useContext

内置
函数签名const value = useContext<T>(Context)

在函数组件里读 Context 的值。上面最近的 Provider 推新值时,组件就会重渲染。

何时用

横切关注点:主题、当前用户、i18n、特性开关。子树里很多组件都要、又不想一级级传 prop 的值。

何时不用

频繁变化的值(每个消费者都会重渲染)。要细粒度更新别用 context 顶替状态库,用 Zustand / Jotai / Redux 的 selector。

常见坑: Provider 的 value 直接写对象字面量,每次都新建一个,所有消费者全部重渲染。用 useMemo 包一下:<Ctx.Provider value={useMemo(() => ({a, b}), [a, b])}>。

例子
const ThemeCtx = createContext<'light' | 'dark'>('light');
function Toolbar() {
  const theme = useContext(ThemeCtx);
  return <div className={theme}>...</div>;
}

useTransition

内置
函数签名const [isPending, startTransition] = useTransition()

把一次 state 更新标记为非紧急。React 会让旧 UI 保持可交互,后台渲染过渡用的新 UI,isPending 告诉你正在进行。

何时用

边输入边过滤长列表。切换 tab,新 tab 渲染很贵的场景。任何渲染慢导致输入/点击发卡的地方。

何时不用

用户期望立刻反映的更新(输入框值、按钮切换)。副作用:startTransition 是给 state 更新用的,不要拿来包 fetch / 定时器。

常见坑: 拿它包 fetch 没用,它只把回调里的 state 更新 延后。数据请求该用 Suspense + use(),useTransition 包随后那次 state 更新。

例子
const [isPending, startTransition] = useTransition();
function onChange(e: ChangeEvent<HTMLInputElement>) {
  setQuery(e.target.value);                 // urgent: input updates now
  startTransition(() => setList(filter(e.target.value))); // non-urgent
}

useDeferredValue

内置
函数签名const deferred = useDeferredValue<T>(value)

返回一个延迟版的值。在昂贵渲染期间,这个延迟版会比真值"慢一拍",让紧急更新先生效。类似 useTransition,但用在你拿不到 setter 的值上(比如 props)。

何时用

收到一个频繁变化的 prop,要用它渲染一个昂贵的视图但不想阻塞输入。配 React.memo 用在那个慢子组件上,只有延迟版变化才触发慢渲染。

何时不用

你拿得到 setter 时,useTransition 更明确。极小的值,推迟反而闪烁、性能也没好处。

常见坑: 昂贵的消费组件没套 React.memo,useDeferredValue 还是会触发慢渲染,只是晚一点。memo 才是真正让它生效的关键。

例子
function Search({ query }: { query: string }) {
  const deferred = useDeferredValue(query);
  const isStale = deferred !== query;
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <HeavyList query={deferred} />
    </div>
  );
}

useSyncExternalStore

内置
函数签名const snapshot = useSyncExternalStore<T>(subscribe, getSnapshot, getServerSnapshot?)

订阅外部(非 React)数据源(Redux store、浏览器 API、observable),并且在并发渲染下不撕裂。返回当前快照,store 通知时触发重渲染。

何时用

为非 React 的数据源(window.matchMedia、document.visibilityState、第三方状态管理库)写自定义 hook。写库的人做并发安全的绑定。

何时不用

普通组件 state(useState)或基于 context 的 store(useContext)。大部分业务代码用不到,它是底层水管。

常见坑: getSnapshot 在没变化时必须返回 引用相同 的值,每次都新建对象会触发无限重渲染。在闭包里缓存快照。

例子
function useOnline() {
  return useSyncExternalStore(
    (cb) => {
      window.addEventListener('online', cb);
      window.addEventListener('offline', cb);
      return () => {
        window.removeEventListener('online', cb);
        window.removeEventListener('offline', cb);
      };
    },
    () => navigator.onLine,
    () => true,  // SSR snapshot
  );
}

useId

内置
函数签名const id = useId(): string

生成一个唯一、确定的 id,服务端和客户端渲染结果一致。设计目的是无障碍属性,把 label 和 input 关联起来。

何时用

aria-labelledby、可复用表单组件里的 htmlFor + id、tooltip 上的 aria-describedby。任何需要"在 SSR 下也一致"的唯一 id。

何时不用

列表的 key(用数据自己的 id)。CSS 选择器或 DOM 查找用的 id,返回的字符串包含 :,做 CSS 选择器要转义。

常见坑: id 里带冒号(类似 `:r1:`),当 HTML 属性没问题,但 querySelector(`#${id}`) 会挂。改用 document.getElementById,它接受任何字符串。

例子
function Field({ label }: { label: string }) {
  const id = useId();
  return <><label htmlFor={id}>{label}</label><input id={id} /></>;
}

useOptimistic

内置
函数签名const [optimistic, addOptimistic] = useOptimistic<T, A>(state, updateFn)

React 19+。在 server action 还在路上时,先把乐观结果展示给用户。返回基于真实 state + 待处理乐观更新算出的临时 state,action 完成后自动对齐。

何时用

点赞 / 投票按钮。发聊天消息。切换设置项,服务端确认很快但 UI 要 0 延迟反馈。

何时不用

服务端慢、容易失败的请求,回滚闪一下比直接转圈更难看。乐观值会被服务端改写的场景(服务器会规范化输入)。

常见坑: 必须在 transition 或 server action 里调用,普通事件 handler 里调没视觉效果。action 一完成乐观值就消失;想保留要同步到本地 state。

例子
const [optimisticLikes, addLike] = useOptimistic(
  likes,
  (current, _: number) => current + 1,
);
async function onLike() {
  addLike(1);                   // UI updates immediately
  await serverLike(postId);     // real call
}

useActionState

内置
函数签名const [state, action, isPending] = useActionState<S, P>(reducerFn, initial, permalink?)

React 19+。管理"通过异步 action 切换"的 state,典型场景是表单提交。把 reducer 风格的更新、自动 pending 标志、表单的渐进增强组合在一起。

何时用

带服务端校验的表单提交(如登录表单返回 {error: "密码错"})。任何想要"自动 pending state"的异步切换,省得手写 useState + try/finally。

何时不用

同步的 state 变化(useState / useReducer 更简单)。不像表单的异步流程,useTransition 更灵活。

常见坑: reducer 签名是 (state, formData) => Promise<state>,不是经典的 (state, action)。action 还没完用户就跳走,isPending 还是 true,在意的话在 effect 里清理。

例子
async function login(prev: State, fd: FormData): Promise<State> {
  const res = await api.login(fd.get('email'), fd.get('password'));
  return res.ok ? { user: res.user } : { error: res.message };
}
const [state, action, isPending] = useActionState(login, { user: null });
return <form action={action}>...</form>;

useFormStatus

内置
函数签名const { pending, data, method, action } = useFormStatus()

React 19+。读取 父级 <form> 的提交状态。设计目的是在表单 内部 的按钮/输入框里调用,自己 disable 或显示 spinner,不用一级级传 prop。

何时用

可复用的 <SubmitButton>,表单提交时自动 disable。字段级别的内联进度提示,不需要知道表单具体怎么实现。

何时不用

不在表单里调(返回 { pending: false, data: null })。已经有 useActionState 的 isPending 时,直接传下去就行。

常见坑: 组件不在 form 内部时,返回 false / null。和 <form> 同级的组件里调,永远是 false,它读的是 父级 form,不是当前组件挂的那个。

例子
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>
    {pending ? 'Saving…' : 'Save'}
  </button>;
}

use

内置
函数签名const value = use<T>(promise | Context)

React 19+。统一的"读"原语:传 Promise 会挂起直到 resolve,传 Context 会读它的值。和别的 hook 不一样,use() 可以在 if / 循环 / 提前 return 里调。

何时用

在 Suspense 边界里读 server-action 的 Promise(React 19 渲染异步数据的标准方式)。条件式读 context(useContext 不能放在条件里)。

何时不用

在组件函数体里 当场 new Promise,每次渲染都是新 Promise,会无限挂起。Promise 要从父级传进来,或者 memo 住。

常见坑: use(somePromise) 读的是 缓存的 promise。如果不稳定引用(在父级稳定、用 cache()、或交给 React Query),会引发 挂起→解决→重渲染 的死循环。

例子
function User({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);   // suspends until resolved
  return <div>{user.name}</div>;
}

// conditional context read — useContext cannot do this
const value = condition ? use(CtxA) : use(CtxB);
自定义 (28)

useToggle

自定义
函数签名const [on, toggle, setOn] = useToggle(initial = false)

布尔 state 的三元组:当前值、引用稳定的 toggle 函数、原始 setter。toggle 忽略参数,可以直接当 onClick 传。

何时用

折叠面板的展开。模态框开关。亮 / 暗主题切换。任何地方写过 `setOpen(o => !o)` 超过一次。

何时不用

非布尔的 state。需要乐观或异步切换的布尔,用 useOptimistic 或 useActionState。

常见坑: toggle 直接传给 onClick 时,TypeScript 可能抱怨 MouseEvent vs void,把签名重载成忽略参数的 () => void。

例子
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn(o => !o), []);
  return [on, toggle, setOn] as const;
}
const [open, toggleOpen] = useToggle();
<button onClick={toggleOpen}>{open ? 'Hide' : 'Show'}</button>

useLocalStorage

自定义
函数签名const [value, setValue] = useLocalStorage<T>(key, initial)

签名和 useState 一样,但会写到 window.localStorage,挂载时回读。要走 JSON 序列化、要防 SSR(window 是 undefined)。

何时用

用户偏好(主题、侧边栏折叠状态)。表单草稿。任何"刷新后还得在",但不该放到服务端的值。

何时不用

敏感数据(DevTools 一打开谁都能看)。大块数据(每个站点上限 5 MB,而且是同步 I/O)。跨标签同步如果没监听 `storage` 事件,做不到。

常见坑: JSON.parse 解析出错会抛,包 try/catch,失败回到 initial。Next.js / Astro 这类 SSR 框架里必须判 typeof window === "undefined",否则 build 直接挂。

例子
function useLocalStorage<T>(key: string, initial: T) {
  const [v, setV] = useState<T>(() => {
    if (typeof window === 'undefined') return initial;
    try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : initial; }
    catch { return initial; }
  });
  useEffect(() => { localStorage.setItem(key, JSON.stringify(v)); }, [key, v]);
  return [v, setV] as const;
}

useDebounce

自定义
函数签名const debounced = useDebounce<T>(value, delayMs)

返回一个"静默 delayMs 毫秒后才更新"的值,每次变化都重置计时器。用来限流由频繁变化的值触发的昂贵动作。

何时用

边输入边搜,但等用户停一下再发 API。表单自动保存草稿。窗口 resize → 重算布局。

何时不用

每一次更新都重要的(受控输入的 value 本身不能 debounce,只 debounce 派生的副作用)。在并发渲染下 useDeferredValue 能达到同样体验的场景。

常见坑: 依赖变化时返回旧值,清理函数一定要清掉计时器。delay 太短等于没做;太长用户以为坏了。搜索场景 200-500 ms 是甜区。

例子
function useDebounce<T>(value: T, delay = 300): T {
  const [v, setV] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setV(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return v;
}

useFetch

自定义
函数签名const { data, error, loading } = useFetch<T>(url, options?)

一次性 GET 请求的小工具 hook。返回 { data, error, loading },url 变化或卸载时取消请求。 不是 React Query / SWR 的替代品,那些做了缓存、重试、去重。

何时用

原型、demo、内部工具,上 React Query 没必要的小场景。读单个不需要缓存的接口。

何时不用

生产应用。被多处读的接口(你最后会手写一份去重)。任何需要缓存、重试、变更的地方。

常见坑: 不写 AbortController,url 变化频繁时会出现 旧 响应覆盖 新 响应的竞态。清理函数里必须 abort。

例子
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    const ctrl = new AbortController();
    setLoading(true);
    fetch(url, { signal: ctrl.signal })
      .then(r => r.json()).then(d => { setData(d); setLoading(false); })
      .catch(e => { if (e.name !== 'AbortError') { setError(e); setLoading(false); } });
    return () => ctrl.abort();
  }, [url]);
  return { data, error, loading };
}

useMediaQuery

自定义
函数签名const matches = useMediaQuery(query: string): boolean

返回某条 CSS media query 当前是否匹配。通过 window.matchMedia 订阅,变化时重渲染。和 useSyncExternalStore 配合可以并发安全。

何时用

移动 / 桌面渲染不同组件。在断点切换布局。通过 (prefers-color-scheme: dark) 检测系统深色模式。

何时不用

纯样式:CSS media query 比 React 重渲染便宜。要测尺寸再决定布局的(用 ResizeObserver)。

常见坑: SSR 下 window 是 undefined 所以返回 false,会引发布局闪一下。用 useSyncExternalStore 包一层并给合理的服务端快照,或者只在客户端渲染。

例子
function useMediaQuery(query: string) {
  const subscribe = (cb: () => void) => {
    const mql = window.matchMedia(query);
    mql.addEventListener('change', cb);
    return () => mql.removeEventListener('change', cb);
  };
  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(query).matches,
    () => false,
  );
}
const isMobile = useMediaQuery('(max-width: 768px)');

useEventListener

自定义
函数签名useEventListener<K>(event: K, handler: (e) => void, target?, options?)

对 addEventListener 的封装,把监听绑到 React 生命周期上,并用 ref 保持 handler 引用最新,而不必每次渲染都重新订阅。

何时用

window 上的快捷键。scroll / resize / online-offline。监听第三方组件的 ref。任何要手写 useEffect + addEventListener + cleanup 的场景。

何时不用

React 自己管理的元素,直接用 onClick / onKeyDown props。高频事件(document 上的 mousemove)不做节流。

常见坑: 不用 handler-in-ref 这个技巧,每次渲染都重新订阅,监听器越攒越多。要显式列依赖,handler 走 ref 保鲜。

例子
function useEventListener<K extends keyof WindowEventMap>(
  event: K,
  handler: (e: WindowEventMap[K]) => void,
) {
  const ref = useRef(handler);
  useEffect(() => { ref.current = handler; }, [handler]);
  useEffect(() => {
    const fn = (e: WindowEventMap[K]) => ref.current(e);
    window.addEventListener(event, fn);
    return () => window.removeEventListener(event, fn);
  }, [event]);
}

useOnClickOutside

自定义
函数签名useOnClickOutside(ref: RefObject<HTMLElement>, handler: (e) => void)

在引用元素 之外 发生 mousedown / touchstart 时调用 handler。关 popover / 下拉 / 模态框的标准写法。

何时用

自定义下拉、popover、提及选择器、tooltip 的"点外面关闭"。

何时不用

原生 <dialog>(浏览器自带 Escape 和外部点击)。Portal 内的组件,ref.contains 检查会假阴性,因为 portal 子节点不在 ref 子树。

常见坑: click 事件触发太晚(目标可能已经被移除)。用 mousedown / pointerdown / touchstart,确保下一个 click handler 跑之前关闭已经发生。

例子
function useOnClickOutside(
  ref: RefObject<HTMLElement>,
  handler: (e: MouseEvent | TouchEvent) => void,
) {
  useEffect(() => {
    const fn = (e: MouseEvent | TouchEvent) => {
      const el = ref.current;
      if (!el || el.contains(e.target as Node)) return;
      handler(e);
    };
    document.addEventListener('mousedown', fn);
    document.addEventListener('touchstart', fn);
    return () => {
      document.removeEventListener('mousedown', fn);
      document.removeEventListener('touchstart', fn);
    };
  }, [ref, handler]);
}

usePrevious

自定义
函数签名const previous = usePrevious<T>(value): T | undefined

返回 上一次 渲染时的值。实现是在 useEffect 里往 ref 写当前值,渲染期读到的就是"上一次"的。

何时用

在新旧值之间做过渡动画。检测"上一次是开,这次是关"这种状态变化。和上次 prop 对比但不想冗余存一份 state。

何时不用

真要 渲染 新旧两个值的(用 useState 或 useReducer)。要派生数据的上一个的(直接派生两份对比即可)。

常见坑: 因为 ref 是在渲染 之后 的 useEffect 里写的,首次渲染返回 undefined。代码里必须显式处理 undefined。

例子
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}
const prevCount = usePrevious(count);
const direction = prevCount === undefined ? 'init' : count > prevCount ? 'up' : 'down';

useWindowSize

自定义
函数签名const { width, height } = useWindowSize(): { width: number; height: number }

返回实时的窗口尺寸。订阅 resize 事件,变化时更新。SSR 安全的版本用 useSyncExternalStore 配服务端快照。

何时用

虚拟列表要 viewport 高度。Canvas / SVG 要按窗口尺寸绘制。CSS 表达不了的"超过断点才渲染某组件"的判断。

何时不用

纯 CSS 响应式:media query 更便宜。元素级别的尺寸(用 ResizeObserver,不是 window resize)。

常见坑: resize 在某些平台 60+ Hz。不做节流,拖动每一像素都会让整个子树重渲染。节流到 16-100 ms,或用 rAF 批处理。

例子
function useWindowSize() {
  const [size, setSize] = useState(() => ({
    width: typeof window === 'undefined' ? 0 : window.innerWidth,
    height: typeof window === 'undefined' ? 0 : window.innerHeight,
  }));
  useEffect(() => {
    let raf = 0;
    const onResize = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        setSize({ width: window.innerWidth, height: window.innerHeight });
      });
    };
    window.addEventListener('resize', onResize);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); };
  }, []);
  return size;
}

useInterval

自定义
函数签名useInterval(callback: () => void, delayMs: number | null)

声明式的 setInterval。delay 传 null 暂停。用 ref 始终调最新的 callback 闭包,而不必每次渲染都重建 interval。

何时用

轮询状态。时钟滴答。动画帧的兜底。任何"通过 state 干净暂停"的重复动作。

何时不用

丝滑动画(用 requestAnimationFrame 配合 vsync)。一次性定时(就 useEffect + setTimeout)。亚毫秒精度(浏览器最小 4 ms,会被节流)。

常见坑: 简单写 `useEffect(() => { setInterval(cb, d); }, [cb])` 每次 cb 引用变就重建 interval,通常每次渲染都变。Dan Abramov 博文里推广的 ref-the-callback 写法才是正解。

例子
function useInterval(cb: () => void, delay: number | null) {
  const ref = useRef(cb);
  useEffect(() => { ref.current = cb; }, [cb]);
  useEffect(() => {
    if (delay === null) return;
    const id = setInterval(() => ref.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}
useInterval(() => setNow(Date.now()), running ? 1000 : null);

useTimeout

自定义
函数签名useTimeout(callback: () => void, delayMs: number | null)

声明式的 setTimeout。delayMs 毫秒后触发一次回调;传 null 取消。和 useInterval 一样,用 ref 保持最新回调,改回调不会重置定时器。

何时用

几秒后自动关掉 toast。延迟一会儿再显示加载圈。触发一次性动作,prop 关掉时还能取消。

何时不用

重复动作(用 useInterval)。动画计时(用 requestAnimationFrame)。一处的 useEffect + setTimeout 已经够清楚的场景。

常见坑: 和 useInterval 一样,把回调放进依赖会让定时器每次渲染重置,结果永远不触发。回调走 ref,依赖只放 delay。

例子
function useTimeout(cb: () => void, delay: number | null) {
  const ref = useRef(cb);
  useEffect(() => { ref.current = cb; }, [cb]);
  useEffect(() => {
    if (delay === null) return;
    const id = setTimeout(() => ref.current(), delay);
    return () => clearTimeout(id);
  }, [delay]);
}
useTimeout(() => setVisible(false), visible ? 4000 : null);

useIsMounted

自定义
函数签名const isMounted = useIsMounted(): () => boolean

返回一个 getter,告诉你组件是否还挂载着。在异步任务末尾 setState 之前判一下,避免老版本的"卸载后更新组件"警告。

何时用

在不方便改成 AbortController 的 await 之后,给 setState 加一道保险。卸载后才回调的第三方接口。

何时不用

fetch 流程,优先 AbortController,它能直接取消请求。当默认套路用:React 18+ 已经移除卸载警告,大多数代码不需要它。

常见坑: 返回布尔快照而不是 getter 是错的,值在渲染时就被定住、会失效。要返回一个调用时才读 ref 的函数。

例子
function useIsMounted() {
  const mounted = useRef(false);
  useEffect(() => {
    mounted.current = true;
    return () => { mounted.current = false; };
  }, []);
  return useCallback(() => mounted.current, []);
}
const isMounted = useIsMounted();
await save(); if (isMounted()) setDone(true);

useUpdateEffect

自定义
函数签名useUpdateEffect(effect: () => (void | (() => void)), deps?: any[])

和 useEffect 一样,但跳过第一次(挂载那次)。只在之后的依赖更新时跑。用一个在首渲染后翻转的 ref 标志实现。

何时用

只在某个值"发生变化"时响应,不在挂载时触发,比如用户改了筛选后再同步 query 参数,或者只在更新时做动画。

何时不用

其实也想要挂载那次的(用普通 useEffect)。"跳过首次"是在掩盖设计问题时,有时 effect 本就不该依赖那个值。

常见坑: React 18 StrictMode 开发模式下挂载会跑两次 effect,简单的布尔标志可能漏判。用初始为 false 的 ref,在 effect 末尾置 true,不要用模块级标志。

例子
function useUpdateEffect(effect: () => void | (() => void), deps?: any[]) {
  const isFirst = useRef(true);
  useEffect(() => {
    if (isFirst.current) { isFirst.current = false; return; }
    return effect();
  }, deps);
}
useUpdateEffect(() => { syncUrl(filter); }, [filter]);

useSet

自定义
函数签名const [set, { add, remove, toggle, has, reset }] = useSet<T>(initial?)

用 state 管理一个 Set,配一组引用稳定的辅助方法。每个方法返回 新 Set(保持不可变),React 才能检测到变化。常用于多选 / 复选框列表。

何时用

表格里选中的行 id。展开的树节点 id。任何频繁问"这个 id 选中了吗"的"成员关系"状态。

何时不用

单个布尔(useState)。在意插入顺序或允许重复的有序数据(用数组)。每帧更新的大 Set(每次重建 Set 是 O(n))。

常见坑: 原地改 Set(set.add(x); setSet(set))不会重渲染,引用没变。一定要建新 Set:setSet(s => new Set(s).add(x))。

例子
function useSet<T>(initial?: Iterable<T>) {
  const [set, setSet] = useState(() => new Set(initial));
  const actions = useMemo(() => ({
    add: (v: T) => setSet(s => new Set(s).add(v)),
    remove: (v: T) => setSet(s => { const n = new Set(s); n.delete(v); return n; }),
    toggle: (v: T) => setSet(s => { const n = new Set(s); n.has(v) ? n.delete(v) : n.add(v); return n; }),
    reset: () => setSet(new Set()),
  }), []);
  return [set, actions] as const;
}

useCounter

自定义
函数签名const [count, { inc, dec, set, reset }] = useCounter(initial = 0, opts?)

数字 state,配引用稳定的 inc/dec/set/reset,可选 min/max 钳制。所有更新都用函数式 setState,连点也不会丢更新。

何时用

数量步进器、分页页码、评分控件,任何"有上下界、操作固定"的整数 state。

何时不用

自由输入的数字(直接 useState + 改变时 parse)。非整数或带单位、钳制逻辑更复杂的值。

常见坑: 钳制要放在 updater 里面,不能放外面:inc 应是 setCount(c => clamp(c + 1))。对捕获的 count 钳制会丢并发更新。

例子
function useCounter(initial = 0, { min = -Infinity, max = Infinity } = {}) {
  const clamp = (n: number) => Math.min(max, Math.max(min, n));
  const [count, setCount] = useState(clamp(initial));
  const actions = useMemo(() => ({
    inc: (step = 1) => setCount(c => clamp(c + step)),
    dec: (step = 1) => setCount(c => clamp(c - step)),
    set: (n: number) => setCount(clamp(n)),
    reset: () => setCount(clamp(initial)),
  }), []);
  return [count, actions] as const;
}

useClipboard

自定义
函数签名const { copy, copied } = useClipboard(resetMs = 1500)

把文本复制到剪贴板,并暴露一个短暂为 true 的 `copied` 标志。封装异步的 navigator.clipboard.writeText,用计时器重置标志。

何时用

代码片段、分享链接、API key 的复制按钮。任何想要"已复制!"提示并自动消失的地方。

何时不用

复制富文本 / 图片(剪贴板 API 要 ClipboardItem,不是 writeText)。非安全上下文,http:// 和老浏览器下 navigator.clipboard 是 undefined。

常见坑: navigator.clipboard.writeText 返回的 Promise 在没有用户手势或权限被拒时会 reject。一定要 .catch,调用前先做特性检测。

例子
function useClipboard(resetMs = 1500) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback(async (text: string) => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      setTimeout(() => setCopied(false), resetMs);
    } catch { setCopied(false); }
  }, [resetMs]);
  return { copy, copied };
}

useHover

自定义
函数签名const [ref, hovered] = useHover<T extends HTMLElement>()

检测指针是否悬在某元素上。返回一个要绑的 ref 和一个布尔值。用 mouseenter / mouseleave,不会像 mouseover 那样因子元素冒泡而误触发。

何时用

行悬停时显示工具条或删除按钮。悬停时懒加载预览。CSS :hover 够不到 React state 的场景。

何时不用

纯视觉的悬停效果(CSS :hover 便宜得多)。纯触屏 UI,触屏没有悬停,要设计点按的替代方案。

常见坑: ref 指向的节点变化时要重新绑监听,得用 ref 回调,不是静态 ref 对象。用 mouseover 而不是 mouseenter,指针掠过子元素时 hovered 会抖。

例子
function useHover<T extends HTMLElement>() {
  const [hovered, setHovered] = useState(false);
  const ref = useRef<T | null>(null);
  useEffect(() => {
    const node = ref.current;
    if (!node) return;
    const on = () => setHovered(true);
    const off = () => setHovered(false);
    node.addEventListener('mouseenter', on);
    node.addEventListener('mouseleave', off);
    return () => {
      node.removeEventListener('mouseenter', on);
      node.removeEventListener('mouseleave', off);
    };
  }, []);
  return [ref, hovered] as const;
}

useIntersectionObserver

自定义
函数签名const [ref, entry] = useIntersectionObserver(options?: IntersectionObserverInit)

用 IntersectionObserver 观察元素进入或离开视口。返回一个 ref 和最新的 IntersectionObserverEntry(entry.isIntersecting 表示是否可见)。

何时用

滚动时懒加载图片 / 组件。无限滚动的哨兵节点。区块滚入视口时只触发一次的动画。曝光埋点。

何时不用

像素级精确的滚动计算(用滚动位置)。要测元素尺寸而不是可见性的(用 ResizeObserver)。没 polyfill 的老浏览器。

常见坑: 每次渲染都重建 observer 会拖垮性能,依赖只放序列化后的 options,清理时 disconnect。observer 是异步的:首个 entry 可能在挂载后一帧才到。

例子
function useIntersectionObserver(options?: IntersectionObserverInit) {
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
  const ref = useRef<HTMLElement | null>(null);
  useEffect(() => {
    const node = ref.current;
    if (!node) return;
    const io = new IntersectionObserver(([e]) => setEntry(e), options);
    io.observe(node);
    return () => io.disconnect();
  }, [options?.root, options?.rootMargin, options?.threshold]);
  return [ref, entry] as const;
}

useDocumentTitle

自定义
函数签名useDocumentTitle(title: string, restoreOnUnmount = false)

把 document.title 设为给定字符串并保持同步。可选在卸载时恢复之前的标题。一个小巧的、用 effect 同步到非 React 全局量的封装。

何时用

在客户端渲染的 SPA 里,把当前路由 / 条目名反映到标签页标题。显示未读数,比如"(3) 收件箱"。

何时不用

SSR / 元框架,用框架的 head API(Next metadata、Astro <title>),爬虫和社交预览才看得到。这里设的标题只在客户端生效。

常见坑: 在渲染期(而非 effect 里)设 document.title 是渲染期副作用,会破坏并发渲染。一定要放在 useEffect 里。

例子
function useDocumentTitle(title: string, restore = false) {
  const prev = useRef(typeof document !== 'undefined' ? document.title : '');
  useEffect(() => {
    document.title = title;
  }, [title]);
  useEffect(() => {
    const previous = prev.current;
    return () => { if (restore) document.title = previous; };
  }, [restore]);
}

useEventCallback

自定义
函数签名const stable = useEventCallback<A extends any[], R>(fn: (...a: A) => R)

返回一个引用稳定、但内部总是调最新 fn 的函数。是提案中 useEffectEvent 的用户态实现,让你在回调里读最新 props/state 而不必加依赖。

何时用

传给很多 memo 子组件、既要引用稳定又要读最新 state 的 handler。避免"函数每次渲染都变"导致的依赖抖动。

何时不用

不要在渲染期调用返回的函数,它故意读最新闭包,只在 effect / 事件 handler 里安全。React 自己要 diff 的值,优先用真实依赖。

常见坑: 渲染期调用会拿到上次提交的值,并发渲染下可能撕裂。React 为此推出了 useEffectEvent,用户态版本只是过渡方案。

例子
function useEventCallback<A extends any[], R>(fn: (...a: A) => R) {
  const ref = useRef(fn);
  useLayoutEffect(() => { ref.current = fn; });
  return useCallback((...args: A) => ref.current(...args), []);
}
const onScroll = useEventCallback(() => log(pageName, window.scrollY));

useThrottle

自定义
函数签名const throttled = useThrottle<T>(value, intervalMs)

返回一个最多每 intervalMs 更新一次的值,哪怕输入变得更快。和 debounce(等静默)不同,throttle 保证稳定的更新节奏。

何时用

由滚动 / mousemove 驱动、必须在手势进行中更新而非停止后才更新的 UI。要按固定频率跳动的实时进度。

何时不用

边输入边搜,只在意停顿后的最终值(用 debounce)。useDeferredValue 已经能平滑重渲染的场景。

常见坑: 只取首次的 throttle,如果最后一个值落在窗口内会被丢,要安排一次尾随更新,别丢最后的值。清理时清掉计时器。

例子
function useThrottle<T>(value: T, interval = 200): T {
  const [throttled, setThrottled] = useState(value);
  const last = useRef(Date.now());
  useEffect(() => {
    const now = Date.now();
    const remaining = interval - (now - last.current);
    if (remaining <= 0) { last.current = now; setThrottled(value); return; }
    const id = setTimeout(() => { last.current = Date.now(); setThrottled(value); }, remaining);
    return () => clearTimeout(id);
  }, [value, interval]);
  return throttled;
}

useScrollLock

自定义
函数签名useScrollLock(locked: boolean)

锁定或解锁页面滚动。锁定时给 documentElement 设 overflow:hidden,解锁 / 卸载时恢复之前的值。模态框和抽屉的标准做法。

何时用

打开模态框 / 抽屉 / 全屏菜单时冻结背景滚动。图片灯箱。

何时不用

用 showModal() 的原生 <dialog>(浏览器已自动阻止背景滚动)。多个锁可能叠加时,要用计数器而不是布尔,否则解锁一个会过早恢复滚动。

常见坑: iOS Safari 上单靠 overflow:hidden 不一定能挡住橡皮筋滚动,可能还要固定 body 定位并恢复 scrollTop。要保存并恢复原始 overflow 值,别假设它是 "auto"。

例子
function useScrollLock(locked: boolean) {
  useLayoutEffect(() => {
    if (!locked) return;
    const root = document.documentElement;
    const prev = root.style.overflow;
    root.style.overflow = 'hidden';
    return () => { root.style.overflow = prev; };
  }, [locked]);
}

useControllableState

自定义
函数签名const [state, setState] = useControllableState<T>({ value, defaultValue, onChange })

让同一个组件 同时 支持受控和非受控用法。传了 props.value 就用它(受控),否则用内部 state(非受控),两种情况都会调 onChange。

何时用

设计系统里可复用的 input / 开关 / select,使用方可能想自己控制、也可能让组件自管,像 Radix 和 React Aria 那样。

何时不用

永远受控或永远非受控的组件,双模式逻辑纯属累赘。深层嵌套的表单 state 交给表单库更好。

常见坑: 在渲染之间让组件在受控和非受控之间切换(value 从 undefined 变成有值)会报警告并重置,要在挂载时根据 value 初值是否定义一次性定下模式。

例子
function useControllableState<T>({ value, defaultValue, onChange }: {
  value?: T; defaultValue?: T; onChange?: (v: T) => void;
}) {
  const isControlled = value !== undefined;
  const [uncontrolled, setUncontrolled] = useState(defaultValue);
  const state = isControlled ? value : uncontrolled;
  const setState = useCallback((next: T) => {
    if (!isControlled) setUncontrolled(next);
    onChange?.(next);
  }, [isControlled, onChange]);
  return [state, setState] as const;
}

useRenderCount

自定义
函数签名const renders = useRenderCount(): number

返回组件渲染了多少次。一个调试用 hook,每次渲染就给 ref 加一。优化时用来抓出意料之外的重渲染。

何时用

性能调试时临时用,确认 memo 是否真的减少了重渲染。开发期打日志,找出渲染风暴。

何时不用

生产代码 / 业务逻辑,渲染次数是实现细节,StrictMode 开发下还会翻倍。绝不要拿它分支真实行为。

常见坑: React 18 StrictMode 开发下组件挂载会渲染两次,所以计数偏高。这里渲染期读并改 ref 没问题,但别依赖它的精确值。

例子
function useRenderCount() {
  const count = useRef(0);
  count.current += 1;
  return count.current;
}
const renders = useRenderCount();
console.log('Table rendered', renders, 'times');

useStateWithHistory

自定义
函数签名const [state, set, { undo, redo, canUndo, canRedo }] = useStateWithHistory<T>(initial)

state 外加一个撤销 / 重做的历史栈。每次 set 压入历史;undo / redo 移动一个指针。指针模型让 redo 一直可用,直到新值分叉时间线。

何时用

编辑器、画布、表单构建器,任何期望 Ctrl+Z / Ctrl+Y 的地方。调试时单步回看 state。

何时不用

又大又频繁变化的 state,不设上限历史会无限占内存。高频更新(给压栈的内容设上限或 debounce)。

常见坑: 指针停在历史中间时设新值,必须 截断 redo 尾部,否则 redo 会重放用户已经放弃的分支。给栈长度设上限,防止内存增长。

例子
function useStateWithHistory<T>(initial: T) {
  const [history, setHistory] = useState<T[]>([initial]);
  const [ptr, setPtr] = useState(0);
  const set = useCallback((value: T) => {
    setHistory(h => [...h.slice(0, ptr + 1), value]);
    setPtr(p => p + 1);
  }, [ptr]);
  const undo = useCallback(() => setPtr(p => Math.max(0, p - 1)), []);
  const redo = useCallback(() => setPtr(p => Math.min(history.length - 1, p + 1)), [history.length]);
  return [history[ptr], set, {
    undo, redo, canUndo: ptr > 0, canRedo: ptr < history.length - 1,
  }] as const;
}

useKeyPress

自定义
函数签名useKeyPress(targetKey: string, handler: (e: KeyboardEvent) => void)

在 window 上按下指定键时调用 handler。匹配 KeyboardEvent.key(如 "Escape"、"Enter"、"k")。用 ref 保持 handler,总能读到最新 state。

何时用

Escape 关模态框。Enter 提交。单键快捷键。配合修饰键判断做 Cmd+K 这类组合。

何时不用

在输入框里打字时,要挡住快捷键,别在聚焦字段时触发。复杂的组合键 / 序列(用专门的快捷键库)。

常见坑: 用 event.key 匹配,不要用 event.keyCode(已废弃)。键值是 "Escape" 不是 "Esc"。不给 handler 套 ref,每次渲染都会重新订阅监听器。

例子
function useKeyPress(targetKey: string, handler: (e: KeyboardEvent) => void) {
  const ref = useRef(handler);
  useEffect(() => { ref.current = handler; }, [handler]);
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === targetKey) ref.current(e);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [targetKey]);
}
useKeyPress('Escape', () => setOpen(false));

useDebouncedCallback

自定义
函数签名const debounced = useDebouncedCallback<A extends any[]>(fn: (...a: A) => void, delayMs)

debounce 一个 回调(不是值):返回一个稳定函数,把对 fn 的调用推迟到静默 delayMs 之后。每次调用重置计时器,最后一次的参数生效。卸载时取消。

何时用

在输入停止的尾沿发 API 调用,同时把事件 / 参数传过去。自动保存,用最新 payload 调 save(payload)。

何时不用

debounce 受控输入的 值(对值用 useDebounce,绝不要 debounce 那个 set 它的 onChange)。需要首沿触发的(这个是尾沿)。

常见坑: 每次渲染返回新函数会让 debounce 失效,计时器要存在 ref 里、返回的函数必须稳定。清理时一定要清掉待执行的计时器,否则频繁卸载会泄漏计时器。

例子
function useDebouncedCallback<A extends any[]>(fn: (...a: A) => void, delay = 300) {
  const fnRef = useRef(fn);
  useEffect(() => { fnRef.current = fn; }, [fn]);
  const timer = useRef<ReturnType<typeof setTimeout>>();
  useEffect(() => () => clearTimeout(timer.current), []);
  return useCallback((...args: A) => {
    clearTimeout(timer.current);
    timer.current = setTimeout(() => fnRef.current(...args), delay);
  }, [delay]);
}
const save = useDebouncedCallback((draft: Draft) => api.save(draft), 800);

useFavicon

自定义
函数签名useFavicon(href: string)

运行时切换页面 favicon,更新(或创建)<link rel="icon"> 元素。一个小巧的、用 effect 同步到 document head 的封装,像 useDocumentTitle 但换的是图标。

何时用

显示带未读 / 告警角标的 favicon。区分环境(staging 用红色图标)。让图标跟随深 / 浅色模式。

何时不用

SSR / 静态 meta,在 HTML head 里设 favicon,水合前就加载好。这个只用于客户端运行时切换。

常见坑: 每次调用都新建 <link> 会堆积重复的图标链接。先查已有的 rel="icon" 复用,没有时才创建一个。

例子
function useFavicon(href: string) {
  useEffect(() => {
    let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
    if (!link) {
      link = document.createElement('link');
      link.rel = 'icon';
      document.head.appendChild(link);
    }
    link.href = href;
  }, [href]);
}
useFavicon(unread > 0 ? '/favicon-alert.ico' : '/favicon.ico');

这个工具能做什么

可搜索的 React hooks 速查表,覆盖 React 18 + 19 自带的 17 个内置 hook,再加上每个项目早晚都会长出来的 10 个常 用自定义 hook。每个 hook 都给你:函数签名、中英双语的 一段说明、明确的"何时用"和"何时不用"对照、真实团队会 踩的那个坑(useEffect 闭包陷阱、useFetch 漏写 AbortController、useSyncExternalStore 的 getSnapshot 每 次返回新对象、useFormStatus 写在表单同级组件上读不 到),再配 1-3 段可直接拷贝的例子。状态(useState、 useReducer)。副作用(useEffect、useLayoutEffect、 useInsertionEffect,这个也讲,顺便告诉你为什么业务代 码几乎用不上)。缓存(useMemo、useCallback,配上「给 所有值套一层是反模式,只会更慢」的提醒)。引用(useRef 和 useImperativeHandle + forwardRef 的配套用法)。上下 文(useContext + Provider value 直接写对象字面量的坑)。 并发(useTransition、useDeferredValue,重子组件不套 React.memo 等于白用)。外部 store(useSyncExternalStore 的并发安全订阅)。无障碍(useId,以及它返回的冒号 id 让 querySelector 挂掉的坑)。React 19 新增四个 (useOptimistic、useActionState、useFormStatus、统一的 use() 读取原语)。再加上从生产代码里捞出来的 10 个自定 义 hook 范式:useToggle、useLocalStorage、useDebounce、 useFetch、useMediaQuery、useEventListener、 useOnClickOutside、usePrevious、useWindowSize、 useInterval。按"内置 / 自定义"筛,搜索一次性跨 hook 名 / 签名 / 说明 / 例子 / 坑,签名行一键复制。完全在浏览器 里跑,不上传不追踪。

工具细节

输入
文件 + 文本
页面会根据工具类型展示文本框、数值控件、文件选择或结构化输入。
输出
即时结果 + 复制 + 预览
结果区优先给出可操作结果,支持项会显示复制、下载或可视化预览。
隐私
可能使用网络查询
组件源码里检测到网络调用,页面会按工具逻辑处理;敏感内容建议先脱敏。
保存 / 分享
本地保存偏好
偏好、历史或草稿保存在本机浏览器,不需要账号。
性能预算
首屏 JS ≤ 30 KB
没有声明 WASM 依赖,适合快速打开和移动端使用。
适用场景
开发运维 · 程序员
分类和职业标签用于推荐相关工具、组织内链,并帮助用户快速判断是否适合当前任务。

怎么用

  1. 1. 输入

    把内容粘贴或拖入工具面板。

  2. 2. 处理

    点击按钮,在浏览器内本地处理,文件不上传。

  3. 3. 复制 / 下载

    一键复制结果或下载到本地。

React Hooks 速查表 适合怎么用

适合穿插在写代码、查问题、做 Review、上线前的小任务里。

适合开发场景

  • 格式化、校验、压缩或检查和代码相关的文本。
  • 把片段整理好再放进文档、工单、提交或交接材料。
  • 不切换工具,快速检查一个小 payload。

开发检查项

  • 压缩、混淆这类不可逆处理,先对副本操作。
  • 除非确认工具本地处理,不要粘贴密钥和敏感片段。
  • 转换后的代码上线前,仍要跑自己的测试或 lint。

下一步可以接着做

这些入口会把当前任务接到更完整的工具链里。

  1. 1 JSON 格式化与校验 浏览器内即时格式化、校验、压缩 JSON,数据不离开本地。 打开
  2. 2 TypeScript 速查表 TypeScript 速查表,100+ 段代码涵盖类型/泛型/工具类型/类型收窄/异步模式。 打开
  3. 3 Python 速查表 Python 速查表,100+ 段地道 Python 代码片段,涵盖字符串/列表/字典/文件/异步,带真实例子。 打开

真实使用场景

  • PR 评审到一半,组里新人问你「useEffect 怎么跑了两次」

    你不在 Slack 里敲一大段,直接甩速查表链接过去。他搜 「useEffect」,看完那 6 行讲严格模式下双调用的坑加清理函数 的修法,顺手拷走 AbortController 那段。PR 评论 4 分钟收尾, 省掉一通 20 分钟的语音,而且接下来两次他自己翻同一条,不 再来问你。

  • 把 React 18 项目升到 19,记不清哪几个 hook 是新的

    动表单代码前,你先筛到 React 19 新增的四个,确认 useFormStatus 必须写在 form 的子组件里(不能和 form 同级), 再照着 useActionState 的签名第一次就把 action 和 pending 接 对。那个恒返回「pending: false」会坑掉的一小时,你直接绕过去。

  • 给三个新同事准备一场半小时的「hooks 串讲」

    你把 17 个内置 hook 摊在一屏上,拿「何时用 / 何时不用」那一 对当讨论引子。useMemo「给所有值套一层只会更慢」那条,和 useDeferredValue 必须配 React.memo 那条,正好引出你想要的争 论,散场时大家手里是同一份能拷的参考。

  • 排查一个死活定位不到的无限重渲染

    屏幕在闪,React 报「re-renders 太多」。你凭直觉搜 「useSyncExternalStore」,看到那条坑:getSnapshot 每次返回新 对象会触发死循环,把快照用稳定引用缓存住,循环立刻停。全程 不到五分钟,一个 Stack Overflow 标签页都没开。

常见踩坑

  • 把 useEffect 当生命周期用,在里头链式 setState。effect 依赖某个 state 又去 setState,会多渲一次甚至死循环;能在渲染期算出来的值就别放 effect,比如 const full = `${first} ${last}`,而不是写个 effect 去 set 它。

  • 「图保险」给每个值都套 useMemo / useCallback。比较和缓存本身要花真实开销;只对算起来贵、或要传给 React.memo 子组件的值缓存,比如 1 万行排序,而不是 useMemo(() => a + b, [a, b])。

  • 在渲染 <form> 的那个组件里调 useFormStatus。它只读父级 form 的状态,所以它的 pending 永远是 false;把提交按钮连同 useFormStatus 挪进 form 的子组件里。

隐私说明

这份速查是一个纯静态页。搜索框里的文字和「内置 / 自定义」筛选全在你浏览器里对内存中的 hook 数组过滤,不发往任何地方。当前的搜索词和筛选会写进 URL,方便你分享或收藏某个视图,但在你自己点分享之前,这个 URL 一直留在你本机。不执行代码,不上传,也不对你输入的内容做任何统计。

常见问题

类似工具组合

做你这行的人, 还会一起用这些。

Made by Toolora · 100% client-side · Updated 2026-07-02