no-use-effect について
React 開発者向けのコーディングスキルです。コンポーネントでの `useEffect` の直接呼び出しを避け、派生状態、イベント、データ取得、購読、マウント時の設定に代替手段を示します。
主な機能
- 01状態と props から値を導出
- 02ユーザー操作をイベントハンドラーで処理
- 03データ取得ライブラリを推奨
- 04購読に `useSyncExternalStore` を使用
- 05マウント時の同期に `useMountEffect` を限定
活用シーン
- 01React コンポーネントの不要な effect の確認
- 02effect ベースのデータ取得の置き換え
- 03マウント時の外部システム設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ alejandrobailo/ no-use-effect --skill no-use-effectインストール後、Claude Code で/no-use-effectを入力して読み込まれたか確認します。
同梱ファイルreferences/patterns.mdREADME.md
詳細
useEffectを直接使用しない
useEffectを直接呼び出してはいけません。マウント時に外部システムと同期するまれなケースでは、useMountEffect()を使用してください。
export function useMountEffect(effect: () => void | (() => void)) {
/* eslint-disable react-hooks/exhaustive-deps, no-restricted-syntax */
useEffect(effect, []);
}
useEffectを直接使用できるのは、再利用可能なカスタムフックの内部(useMountEffect自体や、フェッチライブラリが利用できない場合のuseDataフックなど)だけです。コンポーネントでuseEffectをインポートしたり呼び出したりしてはいけません。
6つのルール
ルール1:状態を同期せず、導出する
既存の状態やpropsから計算できる場合は、インラインで計算してください。
// BAD: two render cycles
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(products.filter((p) => p.inStock));
}, [products]);
// GOOD: one render
const filteredProducts = products.filter((p) => p.inStock);
計算コストが高い場合は、useMemoを使用してください。
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
要注意の兆候: useEffect(() => setX(f(y)), [y])や、ほかの状態やpropsをミラーリングするだけの状態を書こうとしている。
ルール2:データフェッチライブラリを使用する
Effectを使ったフェッチは競合状態を引き起こし、キャッシュ機能を一から作り直すことになります。
// BAD: race condition
useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
// GOOD: library handles cancellation/caching/staleness
const { data: product } = useQuery(
['product', productId],
() => fetchProduct(productId)
);
React 19以降では、use()フックと<Suspense>を使用して、useEffectやuseStateなしでPromiseをアンラップしてください。
// GOOD: use() + Suspense (React 19+)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
要注意の兆候: Effect内でfetch()を実行してからsetState()している。または、キャッシュ、リトライ、キャンセルの仕組みを再実装している。
ルール3:Effectではなくイベントハンドラーを使う
ユーザーの操作がきっかけなら、ハンドラー内で処理してください。手続き型ロジック(検証 → 変換 → 送信)はハンドラーに記述します。refとEffectの連鎖を使って手続きの流れを再現している場合、そのロジックは最初からイベント駆動型だったと考えられます。
// BAD: flag-relay through effect
const [liked, setLiked] = useState(false);
useEffect(() => {
if (liked) { postLike(); setLiked(false); }
}, [liked]);
// GOOD: direct
<button onClick={() => postLike()}>Like</button>
ハンドラー間でロジックを共有する場合は、Effectではなく関数を抽出してください。
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name}`);
}
要注意の兆候: Effectに本来の処理をさせるために状態をフラグとして使っている。または、「フラグを設定 → Effectが実行 → フラグをリセット」という仕組みを使っている。
ルール4:一度だけの外部同期にはuseMountEffectを使う
DOM連携、サードパーティ製ウィジェット、ブラウザーAPIの購読など、マウント時に外部システムをセットアップする真のケースに限ります。
// BAD: guard inside effect
useEffect(() => {
if (!isLoading) playVideo();
}, [isLoading]);
// GOOD: conditional mounting
function VideoPlayerWrapper({ isLoading }) {
if (isLoading) return <LoadingScreen />;
return <VideoPlayer />;
}
function VideoPlayer() {
useMountEffect(() => playVideo());
}
要注意の兆候: 外部システムに対して「マウント時にセットアップし、アンマウント時にクリーンアップする」のが自然な動作になっている。
ルール5:依存関係の調整ではなくkeyでリセットする
「IDが変わったら新しく開始する」必要がある場合は、Reactの再マウントの仕組みを使用してください。
// BAD: effect resets on ID change
useEffect(() => { loadVideo(videoId); }, [videoId]);
// GOOD: key forces clean remount
<VideoPlayer key={videoId} videoId={videoId} />
function VideoPlayer({ videoId }) {
useMountEffect(() => { loadVideo(videoId); });
}
これはフォームの状態のリセットや選択項目のクリアなどにも当てはまります。Effectで状態を初期値に戻すのではなく、コンポーネントにkeyを指定してください。
要注意の兆候: Effectの役割が、IDやpropの変更時にローカル状態をリセットすることだけになっている。
ルール6:壊れたEffectをrefで修正しない
Effectの二重実行、ループ、古い状態へのアクセスを防ぐためにuseRefが必要なら、問題はEffect自体にあります。refで応急処置せず、根本原因であるEffectをなくしてください。
// BAD: ref guard hides the real problem
const hasRun = useRef(false);
useEffect(() => {
if (hasRun.current) return;
hasRun.current = true;
showWelcomeToast(userId);
}, [userId]);
// GOOD: useMountEffect for true one-time setup
useMountEffect(() => {
showWelcomeToast(userId);
});
// BAD: ref to capture latest callback, dodging the dependency array
const onMessageRef = useRef(onMessage);
onMessageRef.current = onMessage;
useEffect(() => {
const conn = createConnection(roomId);
conn.on('message', (msg) => onMessageRef.current(msg));
return () => conn.disconnect();
}, [roomId]);
// GOOD: useEffectEvent (experimental) captures latest values automatically
const onMsg = useEffectEvent((msg) => {
onMessage(msg);
});
useEffect(() => {
const conn = createConnection(roomId);
conn.on('message', onMsg);
return () => conn.disconnect();
}, [roomId]);
Reactのドキュメントでは、次のように明確に警告しています。「正しい問いは『Effectを一度だけ実行するにはどうすればよいか』ではなく、『再マウント後も機能するようにEffectを修正するにはどうすればよいか』です。」
要注意の兆候: hasRun.currentやisMounted.currentを追加している。または、Effectを実行するタイミングや実行の有無を制御することだけを目的としたrefを使っている。あるいは、依存配列に関数を含めないためにコールバックをrefに保存している。
その他のパターン
状態の変更を親に通知する
onChangeを呼び出すためにEffectを使わないでください。イベントハンドラー内で両方を更新するか、状態を親に持ち上げてください。
// BAD
useEffect(() => { onChange(isOn); }, [isOn, onChange]);
// GOOD: update both during the event
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn);
}
最新の値にはuseEffectEventを使う(実験的)
Effect内でpropやstateの最新値を読み取る必要があり、その値が変わってもEffectを再実行したくない場合は、refで回避するのではなくuseEffectEventを使用してください。
// BAD: ref to read latest theme without adding it as dependency
const themeRef = useRef(theme);
themeRef.current = theme;
useEffect(() => {
logVisit(url, themeRef.current);
}, [url]);
// GOOD: useEffectEvent reads latest values automatically
const onVisit = useEffectEvent(() => {
logVisit(url, theme);
});
useEffect(() => {
onVisit();
}, [url]);
useEffectEventが安定版になるまでは、ロジックをイベントハンドラーまたはuseMountEffectに移すことを優先してください。どちらも適さない場合は、refを使った回避策をカスタムフック内に分離し、コンポーネント内には記述しないでください。
DOMの副作用にはコールバックrefを使う
DOMの計測やセットアップには、useRefとuseMountEffectの組み合わせよりも、コールバックrefのほうが信頼性があります。ノードの接続時または切断時に正確に実行されるためです。
// BAD: ref.current may be null on first render
const ref = useRef(null);
useMountEffect(() => {
setHeight(ref.current.getBoundingClientRect().height);
});
return <div ref={ref}>Content</div>;
// GOOD: callback ref fires when node is attached
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return <div ref={measuredRef}>Content</div>;
React 19以降では、コールバックrefからクリーンアップ関数を返せます。それより前のバージョンでは、カスタムフック内のrefにクリーンアップ処理を保存してください。
外部ストアを購読する
手動で購読するEffectではなく、useSyncExternalStoreを使用してください。
const isOnline = useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true
);
アプリの初期化
Effect内ではなく、モジュールレベルで一度だけ実行してください。
if (typeof window !== 'undefined') {
checkAuthToken();
loadDataFromLocalStorage();
}
計算の連鎖
互いを引き起こす副作用を連鎖させないでください。値はその場で導出し、イベントハンドラー内で状態更新をまとめて行います。
// BAD: 3 effects chaining state -> 3 extra renders
useEffect(() => { if (card?.gold) setGoldCardCount(c => c + 1); }, [card]);
useEffect(() => { if (goldCardCount > 3) { setRound(r => r + 1); setGoldCardCount(0); } }, [goldCardCount]);
useEffect(() => { if (round > 5) setIsGameOver(true); }, [round]);
// GOOD: derive + handle in event
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) setGoldCardCount(goldCardCount + 1);
else { setGoldCardCount(0); setRound(round + 1); }
}
}
判断チェックリスト
副作用を書く前に、次の質問に答えてください。
- レンダー中に計算できますか? -> その場で導出するか、
useMemoを使います - ユーザー操作によってトリガーされますか? -> イベントハンドラーを使います
- データを取得していますか? -> データ取得ライブラリ(React Query、SWR など)または React 19 以降の
use()+ Suspense を使います - 外部ストアをサブスクライブしていますか? ->
useSyncExternalStoreを使います - プロパティが変わったときに状態をリセットする必要がありますか? ->
keyプロパティを使います - マウント時に外部システムとの同期が本当に必要ですか? ->
useMountEffectを使います - 副作用の実行タイミングや実行有無を制御するために ref を使っていますか? -> ref を削除し、ロジックをイベントハンドラーに移すか、値を導出するか、
useEffectEventを使います - ノードのマウント時に DOM の副作用が必要ですか? -> コールバック ref を使います
上記のどれにも当てはまらず、それでも useEffect が必要だと思う場合は、詳しい例について references/patterns.md を参照してください。
失敗モード
- useMountEffect の失敗は二者択一で明確です。1 回実行されたか、まったく実行されなかったかのどちらかです
- useEffect を直接使った場合の失敗は、クラッシュに至る前に、不安定な動作、パフォーマンスの問題、ループとして徐々に悪化します
見つけやすいバグを選びましょう。