Archive Note
React Hook 学习 4:从 useState、useEffect 到自定义 Hooks
单独理解 useState、useEffect、useRef 还不够。真实开发里,难点往往不在于“这个 Hook 怎么用”,而在于:
一个业务功能应该怎样拆成清晰的状态、同步逻辑和可复用逻辑。
这一篇的重点,就是把前 3 篇连起来,变成真正能落到项目里的 Hook 设计能力。
一、先分清 3 类东西
一个 React 功能模块,通常会同时包含三类信息:
1. 渲染状态
这些值决定 UI 怎么显示。
例如:
- 是否打开弹窗
- 当前表单值
- 当前列表数据
- loading / error
这些应该优先考虑 useState 或 useReducer。
2. 副作用同步
这些逻辑负责和外部世界交互。
例如:
- 请求接口
- 注册事件
- 订阅数据源
- 同步浏览器 API
这些通常落在 useEffect。
3. 非渲染记忆
这些值需要跨渲染保存,但不该触发 UI 更新。
例如:
- 定时器 id
- 上一次值
- 第三方实例
- 最新回调引用
这些通常用 useRef。
一旦你把这三类东西混在一起,组件就会开始难维护。
二、真实业务例子:搜索框
假设你在做一个搜索框。
第一层:渲染状态
const [keyword, setKeyword] = useState("");
const [results, setResults] = useState<Item[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
第二层:副作用同步
useEffect(() => {
if (!keyword.trim()) {
setResults([]);
return;
}
let active = true;
async function runSearch() {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
const data = await response.json();
if (active) {
setResults(data);
}
} catch (err) {
if (active) {
setError("Search failed");
}
} finally {
if (active) {
setIsLoading(false);
}
}
}
runSearch();
return () => {
active = false;
};
}, [keyword]);
第三层:非渲染记忆
如果你后面要加 debounce,就可能需要:
const timerRef = useRef<number | null>(null);
这就是一个典型的 Hook 组合场景。
三、什么时候应该提取自定义 Hook
很多人提取自定义 Hook 太早,结果只是把复杂度换了个文件名。
更好的判断标准是:
- 这段逻辑是否在多个组件中重复出现
- 这段逻辑是否已经形成清晰职责
- 抽出去后,组件会不会更容易理解
例如一个搜索功能,最终可以抽成:
function useSearch() {
// keyword
// results
// loading
// effect
// handlers
}
这时候页面组件只关心:
- 输入框怎么渲染
- 结果列表怎么渲染
而不再被一堆请求和状态细节淹没。
四、自定义 Hook 不是“高级语法”,而是逻辑封装
很多人学自定义 Hook 时,会误以为它是 React 的某种特殊能力。
其实本质上它只是:
把一段基于 Hook 的可复用逻辑,包装成一个函数。
例如:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
页面里直接用:
const width = useWindowWidth();
这就是把状态、effect、cleanup 组合成了一个更高层次的抽象。
五、什么时候该用 useMemo / useCallback
这两个 Hook 很常见,但也最容易被滥用。
useMemo
适合缓存昂贵计算结果,避免每次 render 都重算。
useCallback
适合缓存函数引用,避免因为引用变化导致子组件无意义更新。
但真正的判断标准不是“项目里大家都在用”,而是:
- 这里真的有性能问题吗?
- 子组件真的在依赖稳定引用吗?
- 这个优化的复杂度值不值得?
如果没有明确收益,不要机械地给每个函数都套 useCallback。
六、真实开发里最常见的组合错误
错误 1:把派生值也存进 state
会制造无意义同步。
错误 2:把事件逻辑塞进 effect
很多逻辑本来应该在按钮点击时执行,却被硬塞到 effect 里,导致依赖关系越来越乱。
错误 3:为了绕过依赖数组,把所有东西塞进 ref
这会让代码“看起来不报错”,但实际数据流变得很难理解。
错误 4:过早抽自定义 Hook
如果逻辑本身还没稳定,抽出去只会让调试更麻烦。
七、一个更稳的 Hook 设计顺序
我更推荐这样做:
- 先写清楚组件需要哪些渲染状态
- 再识别哪些逻辑属于副作用
- 再识别哪些值只是运行时记忆
- 当逻辑职责稳定后,再提取自定义 Hook
这个顺序会比“一上来就抽象”稳很多。
八、这一篇最该记住的结论
useState管渲染状态useEffect管外部同步useRef管非渲染记忆- 自定义 Hook 的价值在于封装职责清晰、可复用的逻辑
useMemo/useCallback是优化工具,不是默认配置
真正好的 React Hook 使用方式,不是把 API 背熟,而是能在一个功能里清楚地区分:
- 哪些数据驱动 UI
- 哪些逻辑是副作用
- 哪些值只是内部记忆
- 哪些东西值得提取成抽象
这才是从“会写 Hook”到“能用 Hook 设计组件”的区别。