こんにちは!フロントエンドエンジニアの朝井です!
モーダルコンポーネントをなんとなく実装していませんか?
React や Vue では「開閉フラグを渡して <Modal>
をレンダリングするだけ」で動作しますが、その内部に API 呼び出しやステートフルなロジックをべた書きすると、非表示状態でも副作用が走り続け、パフォーマンスや意図しないネストしたAPIリクエストを招きます。
この記事では、
- アンチパターンの問題点
- モーダル内部のコンテンツを切り出して必要時にだけマウントする方法
を解説します。
1. よくあるアンチパターン(React)
// 問題:モーダル内部に API や useEffect をベタ書き const UserModal = ({ isOpen, userId }) => { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${userId}`) .then((res) => res.json()) .then((data) => setUser(data)); }, [userId]); return ( <Modal isOpen={isOpen}> {user ? <div>{user.name}</div> : <div>Loading...</div>} </Modal> ); }; const UserList = () => ( <ul> {users.map((u) => ( <li key={u.id}> {u.name} <UserModal isOpen={selectedUserId === u.id} userId={u.id} /> </li> ))} </ul> );
問題点
2. モーダル内部コンテンツを切り出して遅延マウント
ポイント: モーダル本体には表示制御だけを任せ、内部のデータ取得やロジックは別コンポーネントに切り出して、isOpen && <Content>
で必要時にだけマウントします。
// 中身を切り出し const UserDetailContent = ({ userId }) => { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${userId}`) .then((res) => res.json()) .then(setUser); }, [userId]); if (!user) return <div>Loading...</div>; return <div>{user.name}</div>; }; // モーダル本体 const UserModal = ({ isOpen, userId }) => ( <Modal isOpen={isOpen}> {isOpen && <UserDetailContent userId={userId} />} </Modal> ); const UserList = () => ( <ul> {users.map((u) => ( <li key={u.id}> {u.name} <button onClick={() => setSelectedUserId(u.id)}>詳細</button> <UserModal isOpen={selectedUserId === u.id} userId={u.id} /> </li> ))} </ul> );
3. UI ライブラリの動作例
多くの UI ライブラリでは、標準で「モーダルが開くまで子要素をマウントしない(遅延レンダリング)」機能が備わっています。
そのため、単に Modal の内部を切り出すだけで、多くの場合は問題が解決できます。
まとめ
モーダルが開いていなくても事前に API リクエストを実行したいケースなど、あえて遅延マウントを採用しない選択肢もあります。重要なのは、ユーザー体験や要件に合わせて副作用のタイミングとデータ取得戦略を設計することです。
実際に、弊社のあるプロジェクトでもこのアンチパターンが原因で、リスト上に並んだモーダルごとに同じAPIリクエストがナン百回も同時に発生してしまうという深刻な問題が起きていました。
この経験からも、「表示と処理の切り分け」はモーダル設計における最重要ポイントの1つであると、あらためて痛感しています。
終わりに
最後まで読んでいただき、ありがとうございます。
クロスマートではフロントエンドエンジニアやバックエンドエンジニア、PM、BizDevなど一緒に働ける方を募集しています。 ご興味がある方は、是非こちらを御覧ください!