はじめに
前回、「【React】display:noneでページ切り替えたらUXが劇的に改善した」という記事で、SPAでページの状態を保持するためにdisplay: none/blockで切り替える実装を紹介しました。
するとコメントで、こんな指摘をいただきました:
「display: none を使って、DOM を保持したまま非表示する」パターンについて、React 19.2 以降であれば、
<Activity />コンポーネントを使って制御できますよ!https://ja.react.dev/reference/react/Activity
特に「不要な副作用の制御」については、Activity の機能の中に組み込まれているので、「非表示→表示の時にはセットアップ、表示→非表示のときにはクリーンアップ」と勝手に制御してくれます。
なるほど、公式APIがあったのか!
早速、実際に<Activity />コンポーネントに移行してみました。この記事では、その移行方法と、従来のパターンとの違いを紹介します。
目次
- 従来の実装(display:none パターン)
- React 19.2の
<Activity />とは - 移行方法(コード例)
- 何が違うのか
- 動作は同じか
- メリット・デメリット
- まとめ
1. 従来の実装(display:none パターン)
以前紹介した実装では、display: none/blockを手動で切り替えていました:
function App() {
const location = useLocation()
return (
<>
{/* 検索ページ */}
<div style={{
display: location.pathname === '/' ? 'block' : 'none',
position: 'absolute',
left: 0,
right: 0,
// ... その他のスタイル
}}>
<SearchPage />
</div>
{/* マイリストページ */}
<div style={{
display: location.pathname === '/mylist' ? 'block' : 'none',
position: 'absolute',
left: 0,
right: 0,
}}>
<MyListPage />
</div>
{/* 設定ページ */}
<div style={{
display: location.pathname === '/settings' ? 'block' : 'none',
position: 'absolute',
left: 0,
right: 0,
}}>
<SettingsPage />
</div>
</>
)
}
動作:
- すべてのページが常にマウントされたまま
-
display: none/blockでページの表示/非表示を切り替え - スクロール位置やフォーム入力などの状態が自動的に保持される
課題:
-
display: noneの時も、useEffectなどの副作用は動き続ける - タイマーやAPIポーリングを手動で制御する必要がある
// 従来のパターンでの副作用制御(手動)
function SearchPage() {
const location = useLocation()
const isVisible = location.pathname === '/'
useEffect(() => {
if (!isVisible) return // 手動で制御
// 表示中のみAPIポーリング
const interval = setInterval(() => {
fetchData()
}, 30000)
return () => clearInterval(interval)
}, [isVisible])
}
2. React 19.2の<Activity />とは
React 19.2で追加された公式コンポーネントで、UIの一部を表示・非表示に切り替えるためのものです。
公式ドキュメント: https://ja.react.dev/reference/react/Activity
基本的な使い方
import { Activity } from 'react'
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<YourComponent />
</Activity>
modeプロパティ
-
mode='visible': コンポーネントを表示 -
mode='hidden': コンポーネントを非表示(display: none)
重要な特徴
-
DOMは保持される
-
mode='hidden'でも、DOMツリーからは削除されない - 内部状態(state)は保持される
-
-
副作用が自動制御される ⭐
-
mode='hidden'の時:useEffectのクリーンアップ関数が実行される -
mode='visible'の時:useEffectのセットアップ関数が再実行される - 手動で制御する必要がない!
-
3. 移行方法(コード例)
Before(display:none パターン)
import { useLocation } from 'react-router-dom'
function App() {
const location = useLocation()
return (
<>
<div style={{
display: location.pathname === '/' ? 'block' : 'none',
position: 'absolute',
left: 0,
right: 0,
overflowY: 'auto',
overflowX: 'hidden',
}}>
<SearchPage />
</div>
<div style={{
display: location.pathname === '/mylist' ? 'block' : 'none',
position: 'absolute',
left: 0,
right: 0,
}}>
<MyListPage />
</div>
<div style={{
display: location.pathname === '/settings' ? 'block' : 'none',
position: 'absolute',
left: 0,
right: 0,
}}>
<SettingsPage />
</div>
</>
)
}
After(Activity コンポーネント)
import { useLocation } from 'react-router-dom'
import { Activity } from 'react' // ← インポート追加
function App() {
const location = useLocation()
return (
<>
{/* Activity でラップ、display スタイルを削除 */}
<Activity mode={location.pathname === '/' ? 'visible' : 'hidden'}>
<div style={{
position: 'absolute',
left: 0,
right: 0,
overflowY: 'auto',
overflowX: 'hidden',
}}>
<SearchPage />
</div>
</Activity>
<Activity mode={location.pathname === '/mylist' ? 'visible' : 'hidden'}>
<div style={{
position: 'absolute',
left: 0,
right: 0,
}}>
<MyListPage />
</div>
</Activity>
<Activity mode={location.pathname === '/settings' ? 'visible' : 'hidden'}>
<div style={{
position: 'absolute',
left: 0,
right: 0,
}}>
<SettingsPage />
</div>
</Activity>
</>
)
}
変更点のまとめ
- ✅
import { Activity } from 'react'を追加 - ✅ 各ページを
<Activity mode={...}>でラップ - ✅
display: 'block'/'none'スタイルを削除 - ✅ その他のスタイル(position, overflowなど)はそのまま維持
たったこれだけです!
4. 何が違うのか
コードレベルの違い
| 項目 | display:none パターン | Activity パターン |
|---|---|---|
| インポート | 不要 | import { Activity } from 'react' |
| 表示制御 | display: 'block'/'none' |
<Activity mode='visible'/'hidden'> |
| 副作用制御 | 手動(isVisibleチェック) |
自動(Activityが制御) |
| コード量 | やや多い | 簡潔 |
仕組みの違い
display:none パターン
<div style={{ display: isVisible ? 'block' : 'none' }}>
<SearchPage />
</div>
動作:
-
display: noneでCSSレベルで非表示 - Reactコンポーネントはマウントされたまま
-
useEffectなどの副作用は動き続ける ⚠️ - 手動で
isVisibleをチェックして制御が必要
Activity パターン
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<SearchPage />
</Activity>
動作:
-
mode='hidden'でdisplay: noneを設定(同じ) - Reactコンポーネントはマウントされたまま(同じ)
-
useEffectのクリーンアップが自動実行される ✨ - 手動制御不要
副作用制御の違い(具体例)
Before(手動制御が必要)
function SearchPage() {
const location = useLocation()
const isVisible = location.pathname === '/'
useEffect(() => {
if (!isVisible) return // ← 手動チェックが必要
// 表示中のみAPIポーリング
const interval = setInterval(() => {
fetchData()
}, 30000)
return () => clearInterval(interval)
}, [isVisible]) // ← isVisible を依存配列に追加
}
After(自動制御)
function SearchPage() {
// Activity が自動で制御してくれる
useEffect(() => {
// 表示中のみAPIポーリング(自動)
const interval = setInterval(() => {
fetchData()
}, 30000)
return () => clearInterval(interval)
}, []) // ← シンプルな依存配列
}
Activityの挙動:
-
mode='visible'→mode='hidden':クリーンアップ関数が実行される(clearInterval) -
mode='hidden'→mode='visible':セットアップ関数が再実行される(setInterval)
5. 動作は同じか
UXレベル:完全に同じ
| 項目 | display:none | Activity | 結果 |
|---|---|---|---|
| 画面切り替え速度 | ~16ms | ~16ms | ✅ 同じ |
| 状態保持 | ✅ | ✅ | ✅ 同じ |
| スクロール位置 | ✅ 保持 | ✅ 保持 | ✅ 同じ |
| フォーム入力 | ✅ 保持 | ✅ 保持 | ✅ 同じ |
| ちらつき | ❌ なし | ❌ なし | ✅ 同じ |
ユーザーから見た動作は完全に同じです。
開発者体験:Activityの方が良い
| 項目 | display:none | Activity |
|---|---|---|
| コード量 | やや多い | 簡潔 |
| 副作用制御 | 手動 | 自動 ✨ |
| バグのリスク | やや高い | 低い |
| 保守性 | 普通 | 良い |
6. メリット・デメリット
Activity パターンのメリット
-
✅ 副作用が自動制御される
- タイマー、WebSocket、APIポーリングなどが自動停止
- メモリリークのリスクが減る
-
✅ コードが簡潔
-
isVisibleチェックが不要 - 依存配列がシンプル
-
-
✅ 公式API
- Reactチームが保守
- 将来的なReactの最適化に対応
-
✅ 意図が明確
-
<Activity />という名前から、「アクティブ/非アクティブの切り替え」が直感的
-
Activity パターンのデメリット
-
❌ React 19.2以降が必要
- 古いプロジェクトでは使えない
- アップグレードが必要
-
❌ display:noneと完全に同じではない
-
useEffectの挙動が変わる - 既存コードで
isVisibleに依存している場合、リファクタリングが必要
-
どちらを選ぶべきか
| 状況 | 推奨 |
|---|---|
| React 19.2以降を使用 | Activity |
| React 19.2未満 | display:none |
| 新規プロジェクト | Activity |
| 既存プロジェクト(React 19.2以降) | Activity(移行推奨) |
| 既存プロジェクト(React 19.2未満) | display:none(そのまま) |
7. まとめ
移行は簡単
import { Activity } from 'react'-
<Activity mode='visible'/'hidden'>でラップ -
display: 'block'/'none'を削除
たったこれだけで、公式APIを使った実装に移行できます。
UXは同じ、DXが向上
- ✅ ユーザー体験:完全に同じ
- ✅ 開発者体験:副作用の自動制御で、より安全でシンプルに
- ✅ 保守性:公式APIなので、将来的な最適化に対応
React 19.2以降なら、Activityを使おう
従来のdisplay:noneパターンも十分効果的ですが、React 19.2以降を使っているなら、公式の<Activity />コンポーネントへの移行を強く推奨します。
コードがシンプルになり、副作用の制御が自動化されることで、より安全で保守しやすい実装になります。
参考リンク
この記事が「React 19.2の新機能を知りたい」「既存のdisplay:noneパターンから移行したい」という方の参考になれば幸いです!
コメントやフィードバックがあれば、お気軽にどうぞ 👋