no-use-effect

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

No useEffect

禁止使用useEffect

Never call
useEffect
directly. Use derived state, event handlers, data-fetching libraries, or
useMountEffect
instead.
切勿直接调用
useEffect
。请改用派生状态、事件处理程序、数据获取库或
useMountEffect

Quick Reference

快速参考

Instead of useEffect for...Use
Deriving state from other state/propsInline computation (Rule 1)
Fetching data
useQuery
/ data-fetching library (Rule 2)
Responding to user actionsEvent handlers (Rule 3)
One-time external sync on mount
useMountEffect
(Rule 4)
Resetting state when a prop changes
key
prop on parent (Rule 5)
原本使用useEffect的场景替代方案
从其他状态/Props派生状态内联计算(规则1)
获取数据
useQuery
/ 数据获取库(规则2)
响应用户操作事件处理程序(规则3)
挂载时一次性外部同步
useMountEffect
(规则4)
当Props变化时重置状态父组件的
key
属性(规则5)

When to Use This Skill

何时使用本规则

  • Writing new React components
  • Refactoring existing
    useEffect
    calls
  • Reviewing PRs that introduce
    useEffect
  • An agent adds
    useEffect
    "just in case"
  • 编写新的React组件
  • 重构现有的
    useEffect
    调用
  • 评审引入
    useEffect
    的PR
  • Agent无端添加
    useEffect

Workflow

工作流程

1. Identify the useEffect

1. 识别useEffect用途

Determine what the effect is doing -- deriving state, fetching data, responding to an event, syncing with an external system, or resetting state.
确定该effect的作用——派生状态、获取数据、响应事件、与外部系统同步,或是重置状态。

2. Apply the Correct Replacement Pattern

2. 应用正确的替代方案

Use the five rules below to pick the right replacement.
使用以下五条规则选择合适的替代方式。

3. Verify

3. 验证

npm run lint -- --filter=<package>
npm run typecheck -- --filter=<package>
npm run test -- --filter=<package>
npm run lint -- --filter=<package>
npm run typecheck -- --filter=<package>
npm run test -- --filter=<package>

The Escape Hatch: useMountEffect

例外方案:useMountEffect

For the rare case where you need to sync with an external system on mount:
The implementation wraps
useEffect
with an empty dependency array to make intent explicit:
export function useMountEffect(effect: () => void | (() => void)) {
  /* eslint-disable no-restricted-syntax */
  useEffect(effect, []);
}
对于极少数需要在挂载时与外部系统同步的场景:
该实现通过空依赖数组包裹
useEffect
,明确表达意图:
export function useMountEffect(effect: () => void | (() => void)) {
  /* eslint-disable no-restricted-syntax */
  useEffect(effect, []);
}

Replacement Patterns

替代方案

Rule 1: Derive state, do not sync it

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

Most effects that set state from other state are unnecessary and add extra renders.
// 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);
}
Smell test: You are about to write
useEffect(() => setX(deriveFromY(y)), [y])
, or you have state that only mirrors other state or props.
大多数从其他状态设置状态的effect都是不必要的,还会增加额外的渲染次数。
// 不良写法:两次渲染周期——先显示旧数据,再显示过滤后的数据
function ProductList() {
  const [products, setProducts] = useState([]);
  const [filteredProducts, setFilteredProducts] = useState([]);

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

// 良好写法:一次渲染内完成内联计算
function ProductList() {
  const [products, setProducts] = useState([]);
  const filteredProducts = products.filter((p) => p.inStock);
}
检测方法: 你准备编写
useEffect(() => setX(deriveFromY(y)), [y])
,或者你有仅镜像其他状态或Props的状态。

Rule 2: Use data-fetching libraries

规则2:使用数据获取库

Effect-based fetching creates race conditions and duplicated caching logic.
// 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)
  );
}
Smell test: Your effect does
fetch(...)
and then
setState(...)
, or you are re-implementing caching, retries, cancellation, or stale handling.
基于effect的获取方式会导致竞态条件,还会重复实现缓存逻辑。
// 不良写法:存在竞态条件风险
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

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

// 良好写法:查询库处理取消/缓存/过期逻辑
function ProductPage({ productId }) {
  const { data: product } = useQuery(['product', productId], () =>
    fetchProduct(productId)
  );
}
检测方法: 你的effect执行
fetch(...)
然后
setState(...)
,或者你正在重新实现缓存、重试、取消或过期处理逻辑。

Rule 3: Event handlers, not effects

规则3:使用事件处理程序,而非effect

If a user clicks a button, do the work in the handler.
// 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>;
}
Smell test: State is used as a flag so an effect can do the real action, or you are building "set flag -> effect runs -> reset flag" mechanics.
如果用户点击按钮,请在处理程序中执行操作。
// 不良写法:将effect作为动作中继
function LikeButton() {
  const [liked, setLiked] = useState(false);

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

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

// 良好写法:直接的事件驱动动作
function LikeButton() {
  return <button onClick={() => postLike()}>Like</button>;
}
检测方法: 使用状态作为标记,让effect执行实际动作,或者你正在构建“设置标记 -> 执行effect -> 重置标记”的机制。

Rule 4: useMountEffect for one-time external sync

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

Good uses: DOM integration (focus, scroll), third-party widget lifecycles, browser API subscriptions.
// 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());
}
Use
useMountEffect
for stable dependencies (singletons, refs, context values that never change):
// 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);
});
Smell test: You are synchronizing with an external system, and the behavior is naturally "setup on mount, cleanup on unmount."
适用场景:DOM集成(焦点、滚动)、第三方小部件生命周期、浏览器API订阅。
// 不良写法:在effect内添加判断
function VideoPlayer({ isLoading }) {
  useEffect(() => {
    if (!isLoading) playVideo();
  }, [isLoading]);
}

// 良好写法:满足前置条件时再挂载
function VideoPlayerWrapper({ isLoading }) {
  if (isLoading) return <LoadingScreen />;
  return <VideoPlayer />;
}

function VideoPlayer() {
  useMountEffect(() => playVideo());
}
对于稳定依赖项(单例、refs、永不变化的Context值),使用
useMountEffect
// 不良写法:useEffect依赖永不变化的值
useEffect(() => {
  connectionManager.on('connected', handleConnect);
  return () => connectionManager.off('connected', handleConnect);
}, [connectionManager]); // connectionManager是来自Context的单例

// 良好写法:对稳定依赖项使用useMountEffect

useMountEffect(() => {
  connectionManager.on('connected', handleConnect);
  return () => connectionManager.off('connected', handleConnect);
});
检测方法: 你正在与外部系统同步,且行为自然是“挂载时设置,卸载时清理”。

Rule 5: Reset with key, not dependency choreography

规则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} />;
}
Smell test: You are writing an effect whose only job is to reset local state when an ID/prop changes, or you want the component to behave like a brand-new instance for each entity.
// 不良写法:用effect模拟重新挂载行为
function VideoPlayer({ videoId }) {
  useEffect(() => {
    loadVideo(videoId);
  }, [videoId]);
}

// 良好写法:key强制触发清理和重新挂载
function VideoPlayer({ videoId }) {
  useMountEffect(() => {
    loadVideo(videoId);
  });
}

function VideoPlayerWrapper({ videoId }) {
  return <VideoPlayer key={videoId} videoId={videoId} />;
}
检测方法: 你编写的effect唯一作用是当ID/Props变化时重置本地状态,或者你希望组件针对每个实体表现得像全新实例。

Component Structure Convention

组件结构约定

Computed values come after hooks and local state, never via
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>;
}
计算值应在钩子和本地状态之后定义,绝不能通过
useEffect
实现:
export function FeatureComponent({ featureId }: ComponentProps) {
  // 钩子优先
  const { data, isLoading } = useQueryFeature(featureId);

  // 本地状态
  const [isOpen, setIsOpen] = useState(false);

  // 计算值(禁止使用useEffect + setState)
  const displayName = user?.name ?? 'Unknown';

  // 事件处理程序
  const handleClick = () => { setIsOpen(true); };

  // 提前返回
  if (isLoading) return <Loading />;

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