如何优雅地组织业务逻辑?自定义 Hook 全解析
在实际开发中我们经常会遇到一些重复性的逻辑 ,比如:数据获取、表单验证、订阅事件或 WebSocket、浏览器本地存储交互。这些逻辑如果在多个组件中重复编写,不仅会增加代码冗余,还会降低可维护性 。这时,自定义 Hook 就应运而生了------它正是解决这类问题的方法。
一、什么是自定义 Hook?
自定义 Hook 是 React 中一种将组件中可复用的逻辑抽离出来 的方式。它本质上是一个以 use
开头的 JavaScript 函数,通过引用这个函数可以实现在多个组件之间共享状态逻辑或副作用处理逻辑。
换句话说,自定义 Hook 就是封装一组相关 Hook 的组合逻辑,用于复用组件间的状态逻辑。
举个例子:假设你在开发一个网页应用,多个组件都需要知道当前鼠标的坐标(比如做一个"跟随鼠标动画"或"高亮提示"功能)。这时候就可以把这个"获取鼠标位置"的逻辑封装成一个自定义 Hook。
-
注意要点 :自定义 Hook 并不共享状态本身,而是共享状态逻辑。每个使用该 Hook 的组件仍然拥有独立的状态。
或许你会有这样的疑问:自定义 Hook 中确实声明了状态(比如用
useState
),那为什么说它不共享状态本身呢?以
useCounter
自定义 Hook 为例,这个Hook用于统计数量,hook内有一个数据状态countjsfunction useCounter() { const [count, setCount] = useState(0); const increment = () => setCount(c => c + 1); const decrement = () => setCount(c => c - 1); return { count, increment, decrement }; }
在两个组件中分别使用这个 Hook:
jsxfunction ComponentA() { const { count, increment } = useCounter(); return ( <div> <h2>Component A: {count}</h2> <button onClick={increment}>Increment</button> </div> ); } function ComponentB() { const { count, increment } = useCounter(); return ( <div> <h2>Component B: {count}</h2> <button onClick={increment}>Increment</button> </div> ); }
当我们点击
ComponentA
的按钮,只会改变ComponentA
中的count
,点击ComponentB
的按钮,只会改变ComponentB
中的count
为什么会这样
这是因为每次调用
useCounter()
时,React 都会在当前组件的上下文中重新执行这个函数,并为该组件创建一份新的状态(可以理解为闭包)。-
每个组件在调用
useState()
时,React 内部都会为它分配一块"私有内存区域"来保存状态。 -
即使多个组件调用了同一个 Hook,它们各自的状态仍然是隔离的。
-
-
自定义 Hook 的优势
提高代码复用率:将通用逻辑抽取到自定义 Hook 中,避免在多个组件中重复实现相同的副作用或状态管理逻辑。
增强组件职责单一性:组件只负责 UI 层逻辑,数据处理、副作用等都通过 Hook 抽离出去,使得组件更加清晰易读。
更好的测试性与可维护性:Hook 可以单独测试,也可以按需修改而不影响组件结构。
支持跨项目复用:一旦封装成通用 Hook,就可以打包发布到 npm 或内部库中,供多个项目复用。
二、自定义 Hook 的基本结构
创建自定义 Hook 的过程其实很简单,就是定义一个以 use
开头的函数,并在其中使用 React 的内置 Hooks 来实现特定的功能逻辑。
-
设计原则 :每个 Hook 应该只做一件事情。尽量避免在 Hook 内部产生副作用,比如直接操作 DOM 或者执行异步请求等。如果不可避免,请使用
useEffect
。自定义 Hook 应该是纯粹的函数,即给定相同的输入总是返回相同的结果。 -
注意事项
- 命名规范
-
所有自定义 Hook 必须以
use
开头,如useWindowSize
,useInput
,useLocalStorage
-
避免与内置 Hook 冲突,如不要命名为
useState
,useEffect
-
只在顶层调用 Hook,不能在循环、条件语句或嵌套函数中调用 Hook,否则会导致 React 的 Hook 调用顺序混乱,出现不可预知的 bug。
-
明确返回值结构,尽量返回一个对象或数组,方便使用者解构使用。例如:
jsconst { data, loading, error } = useFetch(url);
-
示例
下面是一个简单的例子,演示如何创建一个自定义 Hook 来追踪窗口大小:
jsimport { useState, useEffect } from 'react'; function useWindowSize() { const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); } window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return windowSize; }
然后在任意组件中都可以这样使用:
jsxfunction WindowDisplay() { const size = useWindowSize(); return ( <div> 窗口尺寸:{size.width} x {size.height} </div> ); }
-
三、自定义 Hook 的应用场景
-
数据请求与缓存
我们可以封装一个统一的数据请求 Hook,支持 loading、error、缓存等功能:
jsfunction useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then(res => res.json()) .then(data => { setData(data); setLoading(false); }) .catch(err => { setError(err); setLoading(false); }); }, [url]); return { data, loading, error }; }
-
用户认证状态管理
例如封装一个
useAuth()
Hook,用于检查当前用户是否登录、是否有权限访问某些资源。jsfunction useAuth() { const [isAuthenticated, setIsAuthenticated] = useState(!!localStorage.getItem('token')); function login(token) { localStorage.setItem('token', token); setIsAuthenticated(true); } function logout() { localStorage.removeItem('token'); setIsAuthenticated(false); } return { isAuthenticated, login, logout }; }
-
表单验证逻辑
我们可以封装一个通用的表单 Hook,支持输入绑定、验证规则、错误提示等。
jsfunction useForm(initialState, validate) { const [values, setValues] = useState(initialState); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const handleChange = e => { const { name, value } = e.target; setValues(prev => ({ ...prev, [name]: value })); }; const handleBlur = e => { const { name } = e.target; setTouched(prev => ({ ...prev, [name]: true })); const validationErrors = validate(values); setErrors(validationErrors); }; const handleSubmit = callback => e => { e.preventDefault(); const validationErrors = validate(values); setErrors(validationErrors); if (Object.keys(validationErrors).length === 0) { callback(); } }; return { values, errors, touched, handleChange, handleBlur, handleSubmit }; }