跳到主要内容

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

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

  • 本地处理
  • 分类 开发运维
  • 适合 格式化、校验、压缩或检查和代码相关的文本。
28 个 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);
自定义 (10)

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);

这个工具能做什么

可搜索的 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 代码片段,涵盖字符串/列表/字典/文件/异步,带真实例子。 打开

常见问题

类似工具组合

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

Made by Toolora · 100% client-side · Updated 2026-05-29