
本文讲解如何设计一个安全、可复用的自定义 Hook(如 useProductData),使其能响应 React Query 返回的数据变化,同时规避“Hook 被条件调用”的错误,并确保状态初始化与数据更新逻辑一致。
本文讲解如何设计一个安全、可复用的自定义 hook(如 useproductdata),使其能响应 react query 返回的数据变化,同时规避“hook 被条件调用”的错误,并确保状态初始化与数据更新逻辑一致。
在 React 中,所有 Hooks 必须在组件顶层无条件调用——这是 React 渲染协调机制的核心约束。因此,像 if (!isLoading) useProductData(products) 这样的条件调用会直接触发 ESLint 错误和运行时异常,即使逻辑上看似合理。
正确的解法不是“延迟调用 Hook”,而是让 useProductData 自身具备对空/未就绪数据的容错能力,并利用 useEffect 主动响应 products 的变化,从而实现“数据驱动”的行为同步。
以下是重构后的 useProductData.js,已适配 React Query 的异步数据流:
import { useState, useEffect } from 'react';
// 提取纯函数逻辑,便于测试与复用
const deriveProductMetadata = (products = []) => {
if (!Array.isArray(products) || products.length === 0) {
return { min: 0, max: 0, uniqueBrands: [] };
}
const brands = products.map(({ id, brand }) => ({ id, brand }));
const prices = products.map((item) => item.price);
const min = Math.min(...prices);
const max = Math.max(...prices);
const uniqueBrands = brands.filter(
(item, index, self) =>
index === self.findIndex((t) => t.brand === item.brand)
);
return { min, max, uniqueBrands };
};
function useProductData(products) {
const { min, max, uniqueBrands } = deriveProductMetadata(products);
// 初始化状态:使用 derived 值,避免初始渲染时 NaN 或 undefined
const [filteredValues, setFilteredValues] = useState([min, max]);
const [checkedBrands, setCheckedBrands] = useState({});
// 当 products 变化时,重置价格筛选范围(可选:保留用户选择?需按需扩展)
useEffect(() => {
setFilteredValues([min, max]);
}, [min, max]);
const brandFilterData = {
uniqueBrands,
checkedBrands,
setCheckedBrands,
};
const priceFilterData = {
min,
max,
filteredValues,
setFilteredValues,
};
const isVoucher = products?.every?.((product) =>
product?.name?.includes?.("Voucher")
) ?? false;
return {
filteredValues,
checkedBrands,
products,
title: "Crowlers",
brandFilterData,
priceFilterData,
isVoucher,
};
}
export default useProductData;✅ 关键改进点说明:
-
始终调用 Hook:
useState和useEffect在每次渲染中都执行,符合规则; -
防御性编程:
deriveProductMetadata显式处理null/undefined/空数组,避免.map()报错; -
响应式更新:
useEffect监听min/max(即products衍生值),确保筛选器范围随新数据自动重置; - 逻辑解耦:将计算逻辑抽离为纯函数,提升可测试性与可维护性。
⚠️ 注意事项:
- 若需保留用户手动设置的
filteredValues(例如用户拖动价格滑块后不因新数据重置),应改用useMemo缓存初始值,并在useEffect中添加防抖或条件判断; -
isVoucher使用可选链(?.)和空值合并(??)增强鲁棒性; -
title等静态字段建议通过参数传入,提高 Hook 复用性(如useProductData(products, { title: "Crowlers" }))。
这样设计后,你的 Catalogue.jsx 可保持最简结构,无需任何条件逻辑:
const Catalogue = () => {
const { data: products, isLoading } = useBeers();
const productData = useProductData(products); // ✅ 安全调用
if (isLoading) return <Spinner />;
return (
<Box>
<ProductPage productData={productData} />
</Box>
);
};最终,你获得了一个真正响应式、可组合、符合 React 规范的自定义 Hook —— 它不关心数据来自 useQuery、useSWR 还是本地 useState,只专注将输入数据转化为结构化的业务状态。


















