← 返回题目列表

React 自定义 Hooks 解决什么问题?有哪些设计原则?

高频 中等 第 4 / 27 题 更新于 2026/07/29
ReactHooks自定义Hooks逻辑复用

简化版

自定义 Hooks 用来复用有状态逻辑,而不是复用 UI。它本质是一个以 use 开头的函数,内部可以调用其他 Hooks,并把状态、方法或副作用封装后返回给组件使用。设计时要保持职责单一、返回值清晰、依赖明确。

详细版

示例:

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = () => setValue(v => !v);
  return { value, toggle, setValue };
}

组件使用:

const { value: open, toggle } = useToggle(false);

自定义 Hook 不是共享状态本身,每次调用都有自己独立的 Hook 状态。它适合封装请求、表单、订阅、分页、权限、弹窗控制等逻辑。

完整版教学

一、自定义 Hooks 复用的是有状态逻辑

React 组件里常有一些和 UI 结构无关的逻辑,比如请求、分页、窗口尺寸、订阅、表单校验。 如果每个组件都复制这些逻辑,维护成本会很高。 自定义 Hook 让这些逻辑可以提取成函数复用。

function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });
  // 监听 resize,更新 size
  return size;
}

这里复用的不是 <div> 或按钮,而是“监听窗口尺寸并更新状态”这段逻辑。 如果 6 个组件都需要窗口宽度,抽成 Hook 比复制 6 份 useEffect 更稳。 这也是它和组件复用的根本区别。

二、自定义 Hook 必须遵守 Hooks 规则

自定义 Hook 内部可以调用 useStateuseEffectuseMemo 等 Hook。 也正因为它调用 Hook,所以它自己必须以 use 开头,并且调用位置要保持稳定。 不能在条件、循环、普通回调里随意调用。

function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const on = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener('online', on);
    window.addEventListener('offline', off);
    return () => {
      window.removeEventListener('online', on);
      window.removeEventListener('offline', off);
    };
  }, []);

  return online;
}

命名为 useOnlineStatus 后,React Hooks lint 才能识别并检查它。 这不是命名洁癖,而是工具链能正确保护你的代码。

三、每次调用自定义 Hook 都有独立状态

自定义 Hook 复用逻辑,不自动共享状态。 同一个 Hook 在两个组件里调用,会创建两份独立的 Hook 状态。 这是很多人会误解的地方。

function A() {
  const a = useToggle(false);
}

function B() {
  const b = useToggle(false);
}

a.valueb.value 互不影响。 如果要共享状态,需要把状态提升到共同父组件,或使用 Context、状态管理库、外部 store。 自定义 Hook 只是抽取“怎么管理状态”的逻辑,不是天然全局 store。

四、返回值设计要稳定、清晰、低耦合

自定义 Hook 可以返回数组,也可以返回对象。 简单固定顺序的场景可以返回数组,比如 React 内置 useState。 字段较多或业务语义强时,更推荐返回对象。

function useRequest<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  return { data, loading, error, reload: () => {} };
}
返回形式优点适合场景
数组短、可重命名两三个固定值
对象语义清楚、不怕顺序业务 Hook、字段多
函数暴露命令能力runresetsubmit

如果返回 5 个以上字段,对象通常比数组更容易读。

五、副作用封装要处理依赖和清理

很多自定义 Hook 内部会使用 useEffect。 这时必须认真处理依赖数组和清理函数。 否则 Hook 被复用到多个组件后,问题会被放大。

function useInterval(callback: () => void, delay: number) {
  useEffect(() => {
    const id = window.setInterval(callback, delay);
    return () => window.clearInterval(id);
  }, [callback, delay]);
}

如果 delay=1000,就是每秒执行一次。 如果组件卸载时不清理,定时器还会继续跑,可能造成内存泄漏或重复请求。 Hook 越通用,越要把边界处理完整。

六、自定义 Hook 要避免过度抽象

不是所有重复代码都值得抽 Hook。 如果只是两行简单逻辑,抽出来可能增加跳转成本。 如果逻辑有清晰业务概念、会被多处复用、包含状态或副作用,就更适合抽。

适合抽 Hook:
  usePagination、useRequest、useDebounce、usePermission
不一定适合:
  只拼一个 className、只格式化一个字符串

记忆钩子:组件复用 UI,Hook 复用逻辑;Hook 共享写法,不自动共享状态。

这个边界讲清楚,面试官通常会继续追问状态共享方案。 你可以顺势说 Context、状态库或状态提升。

七、常见误区与追问

  • 误区:自定义 Hook 会让多个组件共享同一份状态。 每次调用 Hook 都有独立状态,共享状态需要 Context、状态提升或外部 store。
  • 误区:普通函数随便调用 useState 也可以。 只有组件或自定义 Hook 能调用 Hook,自定义 Hook 应以 use 开头。
  • 误区:抽 Hook 越多代码越好。 过度抽象会增加跳转和理解成本,应该围绕稳定业务逻辑抽取。
  • 追问:自定义 Hook 和工具函数有什么区别? Hook 可以调用 React Hooks 并管理状态/副作用,工具函数只是普通计算逻辑。
  • 追问:Hook 返回数组还是对象? 固定少量值可用数组,业务字段多或语义强时对象更清楚。
  • 追问:自定义 Hook 里请求接口要注意什么? 要处理 loading、error、取消或忽略过期响应、依赖变化和卸载清理。

八、加强记忆

自定义 Hooks 的核心是“复用有状态逻辑”。回答时先说它不是复用 UI,也不是天然共享状态;再讲必须以 use 开头并遵守 Hooks 规则;最后用 useRequestusePagination 举例,强调依赖、清理和返回值设计。