关于 no-use-effect
这项 React 编码 skill 指导 Agent 不直接调用 useEffect,而是根据任务选择替代方案。内容涵盖派生状态、数据获取、事件处理、挂载时同步和状态重置。
主要功能
- 01用派生值替代状态同步
- 02数据获取库使用建议
- 03用事件处理器响应操作
- 04用 useMountEffect 进行挂载同步
- 05通过 key 重置状态
适用场景
- 01编写新的 React 组件
- 02重构现有 useEffect 调用
- 03审查引入 useEffect 的拉取请求
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ factory-ai/ factory-plugins --skill no-use-effect安装后在 Claude Code 中输入/no-use-effect确认已加载。
详细介绍
不使用 useEffect
绝不要直接调用 useEffect。请改用派生状态、事件处理器、数据获取库或 useMountEffect。
快速参考
- Lint 规则:
no-restricted-syntax(已配置为禁止useEffect) - React 文档:你可能不需要 Effect
- 来源:https://x.com/alvinsng/status/2033969062834045089
| 不要用 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>;
}