safety-eval-service-frontend/.cursor/skills/specs/11-接口与数据层规范/06-禁止Hook依赖监听接口action/SKILL.md

13 KiB
Raw Permalink Blame History

name description
jjb-no-action-in-hook-deps 禁止在 useEffect/useMemo/useCallback 依赖数组中监听接口 Action 或内置函数。在 useEffect、useMemo、useCallback、fetchUserAction 或 state 监听时使用。

接口与数据层规范

禁止在 Hook 依赖数组中监听接口 Action 或内置函数

📌 一句话declareRequest 生成的 Action 以及 Connect 注入的内置函数(dispatchresetModelStatedispatchModelActiongetModelState不得出现在 useEffectuseMemouseCallback 的依赖数组中。去掉即可,只监听 state 数据。

⚠️ 重要约束

  • 禁止useEffectuseMemouseCallback 的依赖数组中监听接口 Action 函数
  • 禁止useEffectuseMemouseCallback 的依赖数组中监听 props.dispatch
  • 禁止useEffectuseMemouseCallback 的依赖数组中监听 props.resetModelState
  • 禁止useEffectuseMemouseCallback 的依赖数组中监听 props.dispatchModelAction
  • 禁止useEffectuseMemouseCallback 的依赖数组中监听 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);

核心原则

  1. 禁止在任何 Hook 依赖中监听 Action 或内置函数useEffectuseMemouseCallback 的依赖数组都不得包含以下函数:

    • Action 函数(fetchUserActiondataListActiongetResourceListActiondeleteXxxAction 等由 declareRequest 生成的)
    • 内置函数(dispatchresetModelStatedispatchModelActiongetModelState
  2. 允许监听 state 数据:可以在依赖数组中监听 state 数据的变化(如 user?.datauser?.userInfouser?.loading

  3. 空依赖数组:如果只需要在组件挂载时调用一次,使用空依赖数组 []

  4. 响应式更新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 频繁变化 → 死循环!
}

死循环过程

  1. Hook 执行,调用 Action → 更新 state
  2. state 更新 → 组件重新渲染
  3. 重新渲染时 Action/内置函数可能创建新引用
  4. 新引用 → Hook 检测到依赖变化 → 重新执行
  5. 回到步骤 1无限循环

其他原因

  1. 函数引用不稳定:这些函数在每次渲染时可能创建新引用,导致 Hook 频繁执行
  2. 不必要的重新执行:造成性能问题
  3. 逻辑混乱:监听函数而不是数据变化,数据流不清晰
  4. 架构设计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 或内置函数引用