最近在处理日志查询功能时,发现整个模块的功能都集中在一个组件中,代码量接近 1600 行 ,逻辑冗余严重,维护难度大。本文将分享我如何将这个组件拆解重构为模块化、可维护的 hooks 和组件,并深入思考 hooks 的抽象边界与职责划分。
🧭 重构背景
在现有项目中,日志查询是高频使用模块。但实现逻辑混乱、功能堆叠严重,导致组件臃肿难扩展。于是决定借此机会重构:
- ✅ 减少重复逻辑
- ✅ 拆分职责清晰的组件
- ✅ 提高维护性与复用性
📋 功能初始化流程梳理
日志查询初始化时,依赖多个来源的数据,优先级如下:
- 从
React-router
的search
参数读取查询参数 - 再尝试从
state
中读取 - 如果都没有,再读取
localStorage
中的缓存 - 在组件卸载时清理缓存
原先这些逻辑都写在 useEffect
里,通过分析后我将其封装为一个可配置初始化顺序的 hook:
ts
interface UseQueryInitialProps<TState, TCached> {
cachedkey?: string;
handleFromSearch?: (search: string) => void;
handleFromState?: (state: TState) => void;
handleFromCached?: (cached: TCached) => void;
onInitEnd?: () => void;
order?: OrderType[]; // 新增顺序参数
}
// 类似日志查询初始化场景hooks,可通过order参数控制初始化顺序
export default function useInitLogQuery<TState, TCached>(props: UseQueryInitialProps<TState, TCached>) {
const { handleFromSearch, handleFromState, handleFromCached, cachedkey, onInitEnd, order } = props;
const { getStorage } = useLogQueryStorage(cachedkey);
const location = useLocation();
const hasInitRef = useRef(false);
useEffect(() => {
if (hasInitRef.current) return;
hasInitRef.current = true;
for (const key of order) {
if (key === 'search' && location?.search && handleFromSearch) {
void handleFromSearch(location.search);
break;
}
if (key === 'state' && location.state && handleFromState) {
void handleFromState(location.state as TState);
break;
}
if (key === 'cached' && getStorage() && handleFromCached) {
handleFromCached(getStorage());
break;
}
}
onInitEnd?.();
}, [location?.search, location?.state]);
}
// 使用
useInitLogQuery<ISearchProps, LogSearchFormValues>({
cachedkey: LOCAL_STORAGE_KEY,
handleFromSearch: () => {
void handleJumpFromSearch(location?.search);
},
handleFromState: () => {
void handleJumpFromState(location.state as ISearchProps);
},
handleFromCached: (cached) => {
void handleCacheToSearch(cached);
},
onInitEnd: () => {
// 初始化结束后清除缓存
clearStorage();
},
order: [OrderType.search, OrderType.state, OrderType.cached], // 按照顺序处理
});
✅ 可配置优先级
✅ 与路由、缓存解耦
✅ 适用于流程化页面初始化
🧩 Hook 拆分方案
除了 useInitLogQuery
,我将其他逻辑也拆解为以下 hooks:
useParams
:表单结构 → 接口查询参数useLogQuery
:查询参数、日志 key 处理、状态管理useLogQueryStorage
:缓存的读写清除useInitLogQuery
:初始化逻辑封装
🧱 查询组件拆解
我们将庞大的查询组件按照 单一职责 拆解为多个部分:
- 查询表单组件
- 日志展示组件
- 日志导出组件
- 交互容器组件
以查询表单组件为例,由于使用了 antd
,我们采用了 Form
进行封装:
ts
const handleSearch = async () => {
try {
setLoading(true);
await onSearch?.(curForm.getFieldsValue());
} finally {
setLoading(false);
}
};
const handleReset = async () => {
try {
setLoading(true);
const resetValues = {
...initialValues,
...defaultFormValues,
};
synchronousFormSet(resetValues);
await onReset?.(resetValues);
} finally {
setLoading(false);
}
};
表单组件不负责实际查询逻辑,只提供查询参数、控制 loading 状态,查询函数由外部传入。
🔄 表单状态同步策略
由于项目使用的 antd
版本不支持自动监听 Form
更新,因此采用 onChange
方案同步外部状态。
ts
// form.setFieldsValue 不能触发onChange,所以需要手动调用onChange
function synchronousFormSet(values: Partial<LogSearchFormValues>) {
curForm.setFieldsValue(values);
onChange?.(curForm.getFieldsValue());
}
<Form
onValuesChange={(_, allValues) => {
onChange?.(allValues);
}}
当前项目没有使用状态驱动 UI 渲染,因此没有使用类似双向绑定的方式(表单变更会触发整个组件更新)。AI 建议可以将表单状态完全交由父组件控制,这样更符合 React 自顶向下的数据流模型,但会牺牲 Form
的缓存优势。
支持传入 formInstance
以获取表单缓存状态,也支持通过 onChange
同步状态,满足不同使用场景。
❗ 过去使用
ref
获取组件内部值的方式不再推荐 ------ 它无法响应数据更新,违背了 React 数据驱动理念。
💡 对 Hooks 的进一步思考
最初我对 hooks 的使用场景理解并不清晰,疑问是:
"是否每写一个组件都要把状态、函数、
useEffect
全部放到 hooks 里?"
我认为:只有具备复用性的逻辑才适合抽成 hooks。但后来得出更深入的理解:
✅ 除了复用,hooks 也用于关注点分离
就算逻辑不可复用,只要它足够复杂、能让组件更清爽,也值得抽成 hook。
参考我得到的 AI 回答截图:

除了复用和纯逻辑场景,关注点分离也是 hooks 抽离的重要动机。即使逻辑耦合、不可复用,也可借助 hooks 将组件体量压缩,让职责更清晰、代码更可读。
✅ 总结
通过本次日志查询模块重构,我学到了:
- 用 Hook 拆解复杂逻辑,让组件更清爽
- Form 状态设计需结合版本能力 ,合理使用
onChange
- 组件之间数据传递应避免使用 ref,保持数据流一致性
- Hook 不只是复用,更是关注点分离