skills/react-no-useeffect/SKILL.md
React の useEffect を避け、より適切なパターンに置き換えるためのガイド。useEffect を含むコードを書こうとしているとき、またはコードレビューで useEffect を検出したとき、代替パターンを提案するために使う。
npx skillsauth add myuon/agent-skills react-no-useeffectInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
参考: You Might Not Need an Effect
| ケース | NG (useEffect) | OK (代替) | |--------|---------------|-----------| | データフェッチ | useEffect + useState | loader / SWR / React Query / use() | | 派生状態 | useEffect で state を同期 | useMemo / レンダー中の計算 | | イベントへの反応 | useEffect で変更を検知 | イベントハンドラ内で直接処理 | | 外部ストアとの同期 | useEffect + subscribe | useSyncExternalStore | | props/state リセット | useEffect で key 変更を検知 | key prop でコンポーネントを再マウント | | 親への通知 | useEffect で親の setState | イベントハンドラ内で親のコールバックを呼ぶ |
// NG
function Profile({ id }: { id: string }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUser(id).then(setData);
}, [id]);
return <div>{data?.name}</div>;
}
// OK: React Query
function Profile({ id }: { id: string }) {
const { data } = useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id) });
return <div>{data?.name}</div>;
}
// OK: React Router loader
export async function loader({ params }: { params: { id: string } }) {
return fetchUser(params.id);
}
function Profile() {
const data = useLoaderData();
return <div>{data?.name}</div>;
}
// NG
function TodoList({ todos, filter }: { todos: Todo[]; filter: string }) {
const [filtered, setFiltered] = useState(todos);
useEffect(() => {
setFiltered(todos.filter((t) => t.status === filter));
}, [todos, filter]);
return <ul>{filtered.map(/*...*/)}</ul>;
}
// OK: レンダー中に計算
function TodoList({ todos, filter }: { todos: Todo[]; filter: string }) {
const filtered = todos.filter((t) => t.status === filter);
return <ul>{filtered.map(/*...*/)}</ul>;
}
// OK: 高コストなら useMemo
function TodoList({ todos, filter }: { todos: Todo[]; filter: string }) {
const filtered = useMemo(() => todos.filter((t) => t.status === filter), [todos, filter]);
return <ul>{filtered.map(/*...*/)}</ul>;
}
// NG
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
showToast("送信完了");
}
}, [submitted]);
return <button onClick={() => setSubmitted(true)}>送信</button>;
}
// OK: イベントハンドラ内で処理
function Form() {
const handleSubmit = () => {
submitForm();
showToast("送信完了");
};
return <button onClick={handleSubmit}>送信</button>;
}
// NG
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return <div>{width}</div>;
}
// OK: useSyncExternalStore
function WindowWidth() {
const width = useSyncExternalStore(
(cb) => { window.addEventListener("resize", cb); return () => window.removeEventListener("resize", cb); },
() => window.innerWidth,
);
return <div>{width}</div>;
}
// NG
function Chat({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
setMessages([]);
}, [roomId]);
return <MessageList messages={messages} />;
}
// OK: key prop で再マウント
function ChatPage({ roomId }: { roomId: string }) {
return <Chat key={roomId} roomId={roomId} />;
}
以下は useEffect を使うべき正当なケース:
useEffect を見つけたら以下を確認する:
development
React Router v7 の loader パターンのリファレンス。画面表示に伴うデータフェッチに loader を使うことでデータの流れを一方向にし、コードの見通しを良くし、不要な useEffect の利用を抑制する。
development
LLM Key Ring (lkr) - macOS Keychainを使ったAPIキー管理ツールのリファレンス。APIキーの登録・取得・一覧・環境変数注入が必要なときに使う。
tools
GitHub CLI (gh) で Issue・Sub-issue・PR・CI を操作するためのクイックリファレンス
tools
ユーザーがタスク一覧に対して実装を求めてきた際に参照するスキル。複数のイシューを同時に取り組むためのワークフロー。