
本文讲解如何设计一个安全、可复用的自定义 Hook(如 useProductData),使其能正确响应 React Query 的异步数据(如 useBeers 返回的 products),避免条件调用 Hook 的错误,并确保状态与数据源同步。
本文讲解如何设计一个安全、可复用的自定义 hook(如 `useproductdata`),使其能正确响应 react query 的异步数据(如 `usebeers` 返回的 `products`),避免条件调用 hook 的错误,并确保状态与数据源同步。
在 React 中,自定义 Hook 必须无条件调用——这是 React Hooks 规则的核心要求。因此,不能将 useProductData(products) 写在 if (isLoading) return ... 之后,否则会违反“Hooks 调用顺序必须一致”的约定,导致运行时崩溃。
正确的解法是:让 useProductData 自身具备对空/未就绪数据的容错能力,并通过 useEffect 或计算逻辑主动响应 products 的变化。以下是优化后的完整实践方案:
✅ 推荐实现:支持延迟初始化 + 响应式更新
// hooks/useProductData.js
import { useState, useEffect } from 'react';
// 纯函数:从 products 安全提取衍生数据(无副作用)
const extractProductMetadata = (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 };
};
export default function useProductData(products) {
const { min, max, uniqueBrands } = extractProductMetadata(products);
// 初始化过滤区间(即使 products 初始为 undefined/null,useState 仍安全)
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 = Array.isArray(products)
? products.every((product) => product.name?.includes('Voucher'))
: false;
return {
filteredValues,
checkedBrands,
products,
title: 'Crowlers',
brandFilterData,
priceFilterData,
isVoucher,
};
}✅ 在组件中安全使用(无条件调用)
// Catalogue.jsx
import { Box, Spinner } from '@chakra-ui/react';
import ProductPage from '../../components/Products/ProductPage';
import useBeers from '../../hooks/useBeers';
import useProductData from '../../hooks/useProductData';
const Catalogue = () => {
const { data: products, isLoading, error } = useBeers();
// ✅ 安全:始终调用,传入可能为 undefined 的 products
const productData = useProductData(products);
if (isLoading) return <Spinner />;
if (error) return <Box>加载失败:{error.message}</Box>;
return (
<Box>
<ProductPage productData={productData} />
</Box>
);
};
export default Catalogue;⚠️ 关键注意事项
-
不要在自定义 Hook 内直接对
products做.map()等操作而不加空值校验:React Query 的data初始为undefined,首次渲染时products?.map(...)会报错。extractProductMetadata中的products = []默认值和Array.isArray校验是防御性编程的关键。 -
useState初始化值可基于products计算,但需确保该计算是纯且无副作用的;而后续同步(如重置滑块范围)应交由useEffect处理。 -
如需保留用户交互状态(如已勾选的品牌)跨数据刷新,可将
setCheckedBrands的初始值改为useMemo缓存的上一次值,或使用useReducer管理更复杂的状态生命周期。
✅ 总结
让自定义 Hook 依赖 React Query 数据的本质,不是“等待数据再调用”,而是设计为对 undefined / null / 空数组健壮,并通过 useEffect 响应数据变更。这既符合 Hooks 规则,又保持了逻辑内聚与可测试性——extractProductMetadata 可单独单元测试,useProductData 专注状态管理与副作用协调。


















