🎉
React公式ドキュメントに学ぶ:useEffectを減らす実践的アプローチ
React公式ドキュメントの「そのエフェクトは不要かも」は、useEffectの過剰使用を避けるための重要な指針を提供しています。本記事では、公式ドキュメントの具体例を通じて、より良いReactコードを書くための実践的なアプローチを解説します。
なぜuseEffectを減らすべきか
useEffectは強力なツールですが、過剰に使用すると以下の問題が発生します:
- 不要な再実行: 依存関係の変更により予期しないタイミングで実行される
- パフォーマンスの低下: 不必要な処理やレンダリングが発生
- コードの複雑化: データフローが追いにくくなる
- バグの温床: タイミングに依存したバグが発生しやすい
実践例:useEffectを使わないパターン
1. イベントハンドラーへの移動
// 問題のあるコード
function Form() {
const [message, setMessage] = useState('');
// ❌ メッセージが変更されるたびに送信される
useEffect(() => {
sendMessage(message);
}, [message]);
return (
<form>
<input value={message} onChange={e => setMessage(e.target.value)} />
</form>
);
}
// 改善されたコード
function Form() {
const [message, setMessage] = useState('');
function handleSubmit(e) {
e.preventDefault();
// ✅ フォーム送信時のみ実行される
sendMessage(message);
setMessage('');
}
return (
<form onSubmit={handleSubmit}>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit">送信</button>
</form>
);
}
ポイント: ユーザーアクションに対する処理は、useEffectではなくイベントハンドラーで処理する
2. 計算結果のキャッシュ(useMemoの活用)
// 問題のあるコード
function TodoList({ todos, filter }) {
const [visibleTodos, setVisibleTodos] = useState([]);
// ❌ todosやfilterが変更されるたびに実行される
useEffect(() => {
setVisibleTodos(todos.filter(todo => todo.status === filter));
}, [todos, filter]);
return <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}
// 改善されたコード
function TodoList({ todos, filter }) {
// ✅ 計算結果を直接メモ化
const visibleTodos = useMemo(
() => todos.filter(todo => todo.status === filter),
[todos, filter]
);
return <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}
ポイント: 派生状態の計算にはuseEffectではなくuseMemoを使用する
3. keyを使ったstateのリセット
// 問題のあるコード
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// ❌ userIdが変更されるたびにコメントをリセット
useEffect(() => {
setComment('');
}, [userId]);
return <CommentForm comment={comment} onChange={setComment} />;
}
// 改善されたコード
function ProfilePage({ userId }) {
return (
// ✅ keyが変更されると、コンポーネント全体が再作成される
<CommentForm key={userId} />
);
}
function CommentForm() {
const [comment, setComment] = useState('');
return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
ポイント: propsの変更に応じてstateをリセットしたい場合は、keyを使ってコンポーネントインスタンスを制御する
4. 非同期処理の適切な配置
// 問題のあるコード
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// ❌ 複雑な非同期ロジックがuseEffect内に散在
let cancelled = false;
setLoading(true);
searchAPI(query).then(data => {
if (!cancelled) {
setResults(data);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [query]);
return loading ? <Spinner /> : <ResultsList results={results} />;
}
// 改善されたコード(React Queryパターン)
function SearchResults({ query }) {
// ✅ データフェッチングライブラリを活用
const { data: results, isLoading } = useQuery({
queryKey: ['search', query],
queryFn: () => searchAPI(query),
enabled: !!query
});
return isLoading ? <Spinner /> : <ResultsList results={results} />;
}
ポイント: データフェッチングにはReact QueryやSWRなどの専用ライブラリを活用する
useEffectが本当に必要な場合
以下のケースではuseEffectの使用が適切です:
- 外部システムとの同期
- WebSocket接続の管理
- ブラウザAPIの購読(例:window.addEventListenerなど)
- サードパーティライブラリの初期化
- 分析やロギング
- ページビューの記録
- パフォーマンス計測
function ChatRoom({ roomId }) {
useEffect(() => {
// ✅ 外部システムとの同期には適切
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
まとめ:useEffectを減らすための指針
- イベントに反応する処理 → イベントハンドラーで処理
- propsやstateから計算できる値 → useMemoやレンダリング中に計算
- propsの変更でstateをリセット → key属性を活用
- データフェッチング → 専用ライブラリを使用
- 外部システムとの同期 → useEffectを使用(これは適切)
これらのパターンを理解し適用することで、より予測可能で保守しやすいReactコードを書くことができます。useEffectは強力なツールですが、「本当に必要か?」を常に問いかけることが重要です。
参考
Discussion