no-use-effect
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNo useEffect
禁止使用useEffect
Never call directly. Use derived state, event handlers, data-fetching libraries, or instead.
useEffectuseMountEffect切勿直接调用。请改用派生状态、事件处理程序、数据获取库或。
useEffectuseMountEffectQuick Reference
快速参考
- Lint rule: (configured to ban
no-restricted-syntax)useEffect - React docs: You Might Not Need an Effect
- Origin: https://x.com/alvinsng/status/2033969062834045089
| Instead of useEffect for... | Use |
|---|---|
| Deriving state from other state/props | Inline computation (Rule 1) |
| Fetching data | |
| Responding to user actions | Event handlers (Rule 3) |
| One-time external sync on mount | |
| Resetting state when a prop changes | |
- 代码检查规则:(配置为禁用
no-restricted-syntax)useEffect - React文档:You Might Not Need an Effect
- 来源:https://x.com/alvinsng/status/2033969062834045089
| 原本使用useEffect的场景 | 替代方案 |
|---|---|
| 从其他状态/Props派生状态 | 内联计算(规则1) |
| 获取数据 | |
| 响应用户操作 | 事件处理程序(规则3) |
| 挂载时一次性外部同步 | |
| 当Props变化时重置状态 | 父组件的 |
When to Use This Skill
何时使用本规则
- Writing new React components
- Refactoring existing calls
useEffect - Reviewing PRs that introduce
useEffect - An agent adds "just in case"
useEffect
- 编写新的React组件
- 重构现有的调用
useEffect - 评审引入的PR
useEffect - 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 with an empty dependency array to make intent explicit:
useEffectexport function useMountEffect(effect: () => void | (() => void)) {
/* eslint-disable no-restricted-syntax */
useEffect(effect, []);
}对于极少数需要在挂载时与外部系统同步的场景:
该实现通过空依赖数组包裹,明确表达意图:
useEffectexport 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 , or you have state that only mirrors other state or props.
useEffect(() => setX(deriveFromY(y)), [y])大多数从其他状态设置状态的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);
}检测方法: 你准备编写,或者你有仅镜像其他状态或Props的状态。
useEffect(() => setX(deriveFromY(y)), [y])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 and then , or you are re-implementing caching, retries, cancellation, or stale handling.
fetch(...)setState(...)基于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 for stable dependencies (singletons, refs, context values that never change):
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);
});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 :
useEffectexport 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>;
}计算值应在钩子和本地状态之后定义,绝不能通过实现:
useEffectexport 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>;
}