13 KiB
13 KiB
| name | description |
|---|---|
| jjb-no-action-in-hook-deps | 禁止在 useEffect/useMemo/useCallback 依赖数组中监听接口 Action 或内置函数。在 useEffect、useMemo、useCallback、fetchUserAction 或 state 监听时使用。 |
接口与数据层规范
禁止在 Hook 依赖数组中监听接口 Action 或内置函数
📌 一句话:
declareRequest生成的 Action 以及 Connect 注入的内置函数(dispatch、resetModelState、dispatchModelAction、getModelState)不得出现在useEffect、useMemo、useCallback的依赖数组中。去掉即可,只监听 state 数据。
⚠️ 重要约束:
- 禁止在
useEffect、useMemo、useCallback的依赖数组中监听接口 Action 函数- 禁止在
useEffect、useMemo、useCallback的依赖数组中监听props.dispatch- 禁止在
useEffect、useMemo、useCallback的依赖数组中监听props.resetModelState- 禁止在
useEffect、useMemo、useCallback的依赖数组中监听props.dispatchModelAction- 禁止在
useEffect、useMemo、useCallback的依赖数组中监听props.getModelState
适用范围
| Hook | 是否禁止监听 Action/内置函数 | 原因 |
|---|---|---|
useEffect |
❌ 禁止 | 函数引用变化 → 重复执行 → state 更新 → 引用再变 → 死循环 |
useMemo |
❌ 禁止 | 函数引用变化 → 重新计算 → 返回新值/函数 → 触发下游 Hook → 死循环 |
useCallback |
❌ 禁止 | 函数引用变化 → 重新创建回调 → 回调引用变化 → 触发下游 useEffect → 死循环 |
三者本质相同:这些函数在每次渲染时可能产生新引用,一旦作为 Hook 依赖,就会形成"state 更新 → 重新渲染 → 新引用 → Hook 重新执行 → state 更新"的死循环。
禁止行为
❌ 错误示例:useEffect 依赖中监听 Action 或内置函数
import React, { useEffect } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_USER } from '~/enumerate/namespace';
function UserProfile({ user, fetchUserAction, dataListAction, resetModelState, dispatchModelAction, getModelState }) {
// ❌ 错误:在依赖数组中监听 Action 函数
useEffect(() => {
fetchUserAction({ id: 1 });
}, [fetchUserAction]); // ❌ 禁止!死循环
// ❌ 错误:在依赖数组中监听 Action 函数
useEffect(() => {
dataListAction({ page: 1 });
}, [dataListAction]); // ❌ 禁止!死循环
// ❌ 错误:在依赖数组中监听 resetModelState
useEffect(() => {
resetModelState(NS_USER, { loading: false });
}, [resetModelState]); // ❌ 禁止!死循环
// ❌ 错误:在依赖数组中监听 dispatchModelAction
useEffect(() => {
dispatchModelAction(NS_USER, 'dataListAction', { page: 1 });
}, [dispatchModelAction]); // ❌ 禁止!死循环
// ❌ 错误:在依赖数组中监听 getModelState
useEffect(() => {
const dataSource = getModelState(NS_USER, 'dataSource');
}, [getModelState]); // ❌ 禁止!死循环
return <div>{user.userInfo?.name}</div>;
}
export default Connect([NS_USER], true)(UserProfile);
❌ 错误示例:useCallback 依赖中监听 Action(常见错误!)
import React, { useEffect, useCallback } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_RESOURCE } from '~/enumerate/namespace';
function ResourceList({ resource, getResourceListAction, deleteResourceAction }) {
const [pageIndex, setPageIndex] = useState(1);
const [searchParams, setSearchParams] = useState({});
// ❌ 错误:useCallback 依赖了 Action 函数
const fetchList = useCallback(() => {
const params = { pageIndex, pageSize: 10 };
if (searchParams.name) params.name = searchParams.name;
getResourceListAction(params);
}, [pageIndex, searchParams, getResourceListAction]); // ❌ 禁止!死循环
// fetchList 引用因 getResourceListAction 变化而不稳定 → 触发 useEffect 反复执行
useEffect(() => {
fetchList();
}, [fetchList]);
// ... 渲染
}
export default Connect([NS_RESOURCE], true)(ResourceList);
❌ 错误示例:useMemo 依赖中监听 Action
import React, { useMemo } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_RESOURCE } from '~/enumerate/namespace';
function ResourceList({ resource, getResourceListAction }) {
const [pageIndex, setPageIndex] = useState(1);
// ❌ 错误:useMemo 依赖了 Action 函数
const queryConfig = useMemo(() => ({
fetch: (params) => getResourceListAction(params),
defaultPageSize: 10
}), [getResourceListAction]); // ❌ 禁止!死循环
// ... 渲染
}
export default Connect([NS_RESOURCE], true)(ResourceList);
正确做法
✅ 方式一:依赖数组为空(仅调用一次)
import React, { useEffect } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_USER } from '~/enumerate/namespace';
function UserProfile({ user, fetchUserAction }) {
// ✅ 正确:依赖数组为空,仅在组件挂载时调用一次
useEffect(() => {
fetchUserAction({ id: 1 });
}, []); // ✅ 空依赖数组
return <div>{user.userInfo?.name}</div>;
}
export default Connect([NS_USER], true)(UserProfile);
✅ 方式二:监听 state 数据变化(推荐)
import React, { useEffect } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_USER } from '~/enumerate/namespace';
function UserProfile({ user, fetchUserAction }) {
// ✅ 正确:组件挂载时调用 action
useEffect(() => {
fetchUserAction({ id: 1 });
}, []);
// ✅ 正确:监听 state 数据变化,而不是 action 函数
useEffect(() => {
if (user?.userInfo) {
console.log('用户信息已加载:', user.userInfo);
}
}, [user?.userInfo]); // ✅ 允许监听 state 数据
// ✅ 正确:监听 loading 状态变化
useEffect(() => {
if (!user?.loading && user?.userInfo) {
console.log('请求已完成');
}
}, [user?.loading, user?.userInfo]); // ✅ 允许监听 state 数据
return <div>{user.userInfo?.name}</div>;
}
export default Connect([NS_USER], true)(UserProfile);
✅ 方式三:useEffect 监听 state 组合变化(页面列表场景,推荐)
import React, { useEffect, useState } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_RESOURCE } from '~/enumerate/namespace';
function ResourceList({ resource, getResourceListAction, deleteResourceAction }) {
const [pageIndex, setPageIndex] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [searchParams, setSearchParams] = useState({});
const [sortField, setSortField] = useState(null);
const [sortOrder, setSortOrder] = useState(null);
// ✅ 正确:依赖 state 数据,不依赖 action 函数
useEffect(() => {
const params = { pageIndex, pageSize };
if (searchParams.name) params.name = searchParams.name;
if (sortField) {
params.orderBy = sortField;
params.orderDirection = sortOrder === 'ascend' ? 'ASC' : 'DESC';
}
getResourceListAction(params);
// ✅ 依赖列表只有 state 数据,没有 action 函数
}, [pageIndex, pageSize, searchParams, sortField, sortOrder]);
const handleDelete = async (record) => {
const res = await deleteResourceAction({ ids: [record.id] });
// 删除成功后,通过修改 state 触发 useEffect 刷新列表
if (res.success) setPageIndex(1); // 重置到第一页,触发重新请求
};
// ... 渲染
}
export default Connect([NS_RESOURCE], true)(ResourceList);
✅ 方式四:使用 useMemo 计算衍生数据
import React, { useEffect, useMemo } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_USER } from '~/enumerate/namespace';
function UserProfile({ user, fetchUserAction }) {
useEffect(() => {
fetchUserAction({ id: 1 });
}, []);
// ✅ 正确:useMemo 依赖 state 数据
const displayName = useMemo(() => {
return user?.userInfo?.name || '未加载';
}, [user?.userInfo?.name]); // ✅ 允许监听 state 数据
return <div>{displayName}</div>;
}
export default Connect([NS_USER], true)(UserProfile);
✅ 方式五:页面列表 hooks 标准写法(useCallback 只依赖 state)
import React, { useEffect, useState, useCallback } from 'react';
import { Connect } from '@cqsjjb/jjb-dva-runtime';
import { NS_RESOURCE } from '~/enumerate/namespace';
function ResourceList({ resource, getResourceListAction }) {
const [pageIndex, setPageIndex] = useState(1);
const [searchParams, setSearchParams] = useState({});
// ✅ 正确:useCallback 只依赖 state 数据,不依赖 action
const handleSearch = useCallback((values) => {
setPageIndex(1);
setSearchParams(values);
}, []); // ✅ 空依赖或 state 数据
// ✅ 正确:useEffect 只依赖 state 数据
useEffect(() => {
const params = { pageIndex, pageSize: 10, ...searchParams };
getResourceListAction(params);
}, [pageIndex, searchParams]); // ✅ 只有 state 数据
// ... 渲染
}
export default Connect([NS_RESOURCE], true)(ResourceList);
核心原则
-
禁止在任何 Hook 依赖中监听 Action 或内置函数:
useEffect、useMemo、useCallback的依赖数组都不得包含以下函数:- Action 函数(
fetchUserAction、dataListAction、getResourceListAction、deleteXxxAction等由declareRequest生成的) - 内置函数(
dispatch、resetModelState、dispatchModelAction、getModelState)
- Action 函数(
-
允许监听 state 数据:可以在依赖数组中监听 state 数据的变化(如
user?.data、user?.userInfo、user?.loading) -
空依赖数组:如果只需要在组件挂载时调用一次,使用空依赖数组
[] -
响应式更新:React 会自动响应 state 变化并重新渲染,无需手动监听这些函数
为什么禁止监听这些函数?
⚠️ 核心原因:会造成 React 渲染死循环
监听 Action 或内置函数会导致死循环:
// ❌ 错误示例:useEffect 死循环
function UserProfile({ user, fetchUserAction }) {
useEffect(() => {
fetchUserAction({ id: 1 }); // 调用 action
}, [fetchUserAction]); // 监听 action 函数 → 死循环!
}
// ❌ 错误示例:useCallback 间接死循环(场景)
function ResourceList({ getResourceListAction }) {
const [pageIndex] = useState(1);
const fetch = useCallback(() => {
getResourceListAction({ page: pageIndex });
}, [pageIndex, getResourceListAction]); // ← action 在依赖中!
useEffect(() => { fetch(); }, [fetch]); // fetch 频繁变化 → 死循环!
}
死循环过程:
- Hook 执行,调用 Action → 更新 state
- state 更新 → 组件重新渲染
- 重新渲染时 Action/内置函数可能创建新引用
- 新引用 → Hook 检测到依赖变化 → 重新执行
- 回到步骤 1,无限循环
其他原因
- 函数引用不稳定:这些函数在每次渲染时可能创建新引用,导致 Hook 频繁执行
- 不必要的重新执行:造成性能问题
- 逻辑混乱:监听函数而不是数据变化,数据流不清晰
- 架构设计:Connect 已将 state 注入组件,应监听 state 数据而非函数
对比说明
| 依赖项类型 | useEffect | useMemo | useCallback | 说明 |
|---|---|---|---|---|
fetchUserAction |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | Action 函数 |
dataListAction |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | Action 函数 |
getResourceListAction |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | Action 函数 |
deleteXxxAction |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | Action 函数 |
getModelState |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | 内置函数 |
resetModelState |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | 内置函数 |
dispatchModelAction |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | 内置函数 |
dispatch |
❌ 禁止 | ❌ 禁止 | ❌ 禁止 | 内置函数 |
user?.data |
✅ 允许 | ✅ 允许 | ✅ 允许 | state 数据 |
user?.userInfo |
✅ 允许 | ✅ 允许 | ✅ 允许 | state 数据 |
user?.loading |
✅ 允许 | ✅ 允许 | ✅ 允许 | state 数据 |
order?.list |
✅ 允许 | ✅ 允许 | ✅ 允许 | state 数据 |
[] |
✅ 允许 | — | ✅ 允许 | 空依赖数组 |
总结
- ❌ 禁止:
useEffect(() => {}, [dataListAction])— 不能监听 Action 或内置函数 - ❌ 禁止:
useCallback(() => {}, [getListAction])— 不能监听 Action 或内置函数 - ❌ 禁止:
useMemo(() => {}, [deleteAction])— 不能监听 Action 或内置函数 - ✅ 允许:
useEffect(() => {}, [user?.data])— 可以监听 state 数据 - ✅ 允许:
useEffect(() => {}, [])— 空依赖数组,仅调用一次 - ✅ 推荐:监听 state 数据变化,而不是 Action 或内置函数引用