no-use-effect について
React のコーディングエージェント向けに、useEffect を直接呼び出さず、目的に応じた代替手段を選ぶスキルです。派生状態、データ取得、イベント処理、マウント時の同期、状態リセットを扱います。
主な機能
- 01状態同期の代わりに派生値を使用
- 02データ取得ライブラリの利用指針
- 03ユーザー操作にイベントハンドラーを使用
- 04マウント時の同期に useMountEffect
- 05key による状態リセット
活用シーン
- 01新しい React コンポーネントの作成
- 02既存の useEffect 呼び出しのリファクタリング
- 03useEffect を追加するプルリクエストのレビュー
インストール
コマンドをコピーしてターミナルで実行します。
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 のドキュメント:エフェクトが不要な場合もあります
- 出典:https://x.com/alvinsng/status/2033969062834045089
| useEffect の代わりに... | 使用するもの |
|---|---|
| 他の状態/props から状態を導出する | インライン計算(ルール 1) |
| データを取得する | useQuery /データ取得ライブラリ(ルール 2) |
| ユーザー操作に応答する | イベントハンドラー(ルール 3) |
| マウント時に一度だけ外部と同期する | useMountEffect(ルール 4) |
| prop の変更時に状態をリセットする | 親の key prop(ルール 5) |
このスキルを使う場面
- 新しい React コンポーネントを作成するとき
- 既存の
useEffect呼び出しをリファクタリングするとき useEffectを追加する PR をレビューするとき- エージェントが「念のため」
useEffectを追加したとき
ワークフロー
1. useEffect の用途を特定する
エフェクトが何をしているか(状態の導出、データ取得、イベントへの応答、外部システムとの同期、状態のリセット)を確認します。
2. 適切な置き換えパターンを適用する
以下の 5 つのルールを使って、適切な置き換え方法を選びます。
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:状態を同期するのではなく、導出する
他の状態から状態を設定するエフェクトのほとんどは不要であり、余分な再レンダリングを発生させます。
// 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:データ取得ライブラリを使用する
エフェクトベースのデータ取得は、競合状態やキャッシュロジックの重複を招きます。
// 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)
);
}
要注意の兆候: エフェクト内で fetch(...) を実行してから setState(...) を呼び出している、またはキャッシュ、再試行、キャンセル、古いデータの処理を独自に実装している場合です。
ルール 3:エフェクトではなくイベントハンドラーを使う
ユーザーがボタンをクリックするなら、処理はハンドラー内で行います。
// 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>;
}
要注意の兆候: エフェクトに実際の処理をさせるために状態をフラグとして使っている、または「フラグを設定 → エフェクトが実行 → フラグをリセット」という仕組みを作っている場合です。
ルール 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、変更されないコンテキスト値)は、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 の変更時にローカル状態をリセットすることだけを目的とするエフェクトを書いている、またはエンティティごとにコンポーネントをまったく新しいインスタンスとして動作させたい場合です。
コンポーネント構造の規約
計算値はフックとローカル状態の後に記述し、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>;
}