mcpField
N
no-use-effect

no-use-effect

github.com/factory-ai开发1171.0k 次安装3 天前更新
代码仓库

关于 no-use-effect

这项 React 编码 skill 指导 Agent 不直接调用 useEffect,而是根据任务选择替代方案。内容涵盖派生状态、数据获取、事件处理、挂载时同步和状态重置。

主要功能

  1. 01用派生值替代状态同步
  2. 02数据获取库使用建议
  3. 03用事件处理器响应操作
  4. 04用 useMountEffect 进行挂载同步
  5. 05通过 key 重置状态

适用场景

  1. 01编写新的 React 组件
  2. 02重构现有 useEffect 调用
  3. 03审查引入 useEffect 的拉取请求

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/factory-ai/factory-plugins --skill no-use-effect

安装后在 Claude Code 中输入/no-use-effect确认已加载。

详细介绍

不使用 useEffect

绝不要直接调用 useEffect。请改用派生状态、事件处理器、数据获取库或 useMountEffect。

快速参考

不要用 useEffect 来……改用
根据其他状态/props 派生状态行内计算(规则 1)
获取数据useQuery / 数据获取库(规则 2)
响应用户操作事件处理器(规则 3)
在挂载时进行一次性外部同步useMountEffect(规则 4)
在 prop 变化时重置状态在父组件上设置 key prop(规则 5)

何时使用此技能

  • 编写新的 React 组件
  • 重构现有的 useEffect 调用
  • 审查引入 useEffect 的 PR
  • Agent “以防万一”添加 useEffect

工作流

1. 找出 useEffect

确定 Effect 的作用——派生状态、获取数据、响应事件、与外部系统同步,还是重置状态。

2. 应用正确的替代模式

使用下面的五条规则选择合适的替代方案。

3. 验证
npm run lint -- --filter=<package>
npm run typecheck -- --filter=<package>
npm run test -- --filter=<package>

例外方案:useMountEffect

在少数需要于挂载时与外部系统同步的情况下:

此实现通过空依赖数组包装 useEffect,以明确表达意图:

export function useMountEffect(effect: () => void | (() => void)) {
  /* eslint-disable no-restricted-syntax */
  useEffect(effect, []);
}

替代模式

规则 1:派生状态,而非同步状态

大多数从其他状态设置状态的 Effect 都是不必要的,还会导致额外渲染。

// BAD: Two render cycles - first stale, then filtered
function ProductList() {
  const [products, setProducts] = useState([]);
  const [filteredProducts, setFilteredProducts] = useState([]);

  useEffect(() => {
    setFilteredProducts(products.filter((p) => p.inStock));
  }, [products]);
}

// GOOD: Compute inline in one render
function ProductList() {
  const [products, setProducts] = useState([]);
  const filteredProducts = products.filter((p) => p.inStock);
}

自检提示: 你正准备编写 useEffect(() => setX(deriveFromY(y)), [y]),或者你有一个只镜像其他状态或 props 的状态。

规则 2:使用数据获取库

基于 Effect 的数据获取会造成竞态条件,并导致缓存逻辑重复。

// BAD: Race condition risk
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetchProduct(productId).then(setProduct);
  }, [productId]);
}

// GOOD: Query library handles cancellation/caching/staleness
function ProductPage({ productId }) {
  const { data: product } = useQuery(['product', productId], () =>
    fetchProduct(productId)
  );
}

自检提示: 你的 Effect 执行了 fetch(...),然后调用 setState(...);或者你正在重新实现缓存、重试、取消或过期数据处理。

规则 3:使用事件处理器,而非 Effect

如果用户点击按钮,就在处理器中执行操作。

// BAD: Effect as an action relay
function LikeButton() {
  const [liked, setLiked] = useState(false);

  useEffect(() => {
    if (liked) {
      postLike();
      setLiked(false);
    }
  }, [liked]);

  return <button onClick={() => setLiked(true)}>Like</button>;
}

// GOOD: Direct event-driven action
function LikeButton() {
  return <button onClick={() => postLike()}>Like</button>;
}

自检提示: 你把状态当作标志,让 Effect 执行实际操作;或者你正在构建“设置标志 -> Effect 运行 -> 重置标志”机制。

规则 4:使用 useMountEffect 进行一次性外部同步

适用场景:DOM 集成(聚焦、滚动)、第三方组件生命周期、浏览器 API 订阅。

// BAD: Guard inside effect
function VideoPlayer({ isLoading }) {
  useEffect(() => {
    if (!isLoading) playVideo();
  }, [isLoading]);
}

// GOOD: Mount only when preconditions are met
function VideoPlayerWrapper({ isLoading }) {
  if (isLoading) return <LoadingScreen />;
  return <VideoPlayer />;
}

function VideoPlayer() {
  useMountEffect(() => playVideo());
}

对稳定的依赖项(单例、ref、永不变化的 context 值)使用 useMountEffect:

// BAD: useEffect with dependency that never changes
useEffect(() => {
  connectionManager.on('connected', handleConnect);
  return () => connectionManager.off('connected', handleConnect);
}, [connectionManager]); // connectionManager is a singleton from context

// GOOD: useMountEffect for stable dependencies

useMountEffect(() => {
  connectionManager.on('connected', handleConnect);
  return () => connectionManager.off('connected', handleConnect);
});

自检提示: 你正在与外部系统同步,而且该行为自然地属于“挂载时设置,卸载时清理”。

规则 5:用 key 重置,而非编排依赖项
// BAD: Effect attempts to emulate remount behavior
function VideoPlayer({ videoId }) {
  useEffect(() => {
    loadVideo(videoId);
  }, [videoId]);
}

// GOOD: key forces clean remount
function VideoPlayer({ videoId }) {
  useMountEffect(() => {
    loadVideo(videoId);
  });
}

function VideoPlayerWrapper({ videoId }) {
  return <VideoPlayer key={videoId} videoId={videoId} />;
}

自检提示: 你正在编写一个唯一作用是在 ID/prop 变化时重置本地状态的 Effect,或者你希望组件对每个实体都表现得像一个全新实例。

组件结构约定

计算值应放在 Hooks 和本地状态之后,绝不要通过 useEffect 计算:

export function FeatureComponent({ featureId }: ComponentProps) {
  // Hooks first
  const { data, isLoading } = useQueryFeature(featureId);

  // Local state
  const [isOpen, setIsOpen] = useState(false);

  // Computed values (NOT useEffect + setState)
  const displayName = user?.name ?? 'Unknown';

  // Event handlers
  const handleClick = () => { setIsOpen(true); };

  // Early returns
  if (isLoading) return <Loading />;

  // Render
  return <Flex direction="column" gap="lg">...</Flex>;
}

评论

支持 Markdown