0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React 19.2】display:noneパターンから公式の <Activity /> コンポーネントに移行した話

Posted at

はじめに

前回、「【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 />コンポーネントに移行してみました。この記事では、その移行方法と、従来のパターンとの違いを紹介します。

目次

  1. 従来の実装(display:none パターン)
  2. React 19.2の<Activity />とは
  3. 移行方法(コード例)
  4. 何が違うのか
  5. 動作は同じか
  6. メリット・デメリット
  7. まとめ

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

重要な特徴

  1. DOMは保持される

    • mode='hidden'でも、DOMツリーからは削除されない
    • 内部状態(state)は保持される
  2. 副作用が自動制御される

    • 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>
    </>
  )
}

変更点のまとめ

  1. import { Activity } from 'react' を追加
  2. ✅ 各ページを <Activity mode={...}> でラップ
  3. display: 'block'/'none' スタイルを削除
  4. ✅ その他のスタイル(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>

動作:

  1. display: noneでCSSレベルで非表示
  2. Reactコンポーネントはマウントされたまま
  3. useEffectなどの副作用は動き続ける ⚠️
  4. 手動でisVisibleをチェックして制御が必要

Activity パターン

<Activity mode={isVisible ? 'visible' : 'hidden'}>
  <SearchPage />
</Activity>

動作:

  1. mode='hidden'display: noneを設定(同じ)
  2. Reactコンポーネントはマウントされたまま(同じ)
  3. useEffectのクリーンアップが自動実行される
  4. 手動制御不要

副作用制御の違い(具体例)

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 パターンのメリット

  1. 副作用が自動制御される

    • タイマー、WebSocket、APIポーリングなどが自動停止
    • メモリリークのリスクが減る
  2. コードが簡潔

    • isVisibleチェックが不要
    • 依存配列がシンプル
  3. 公式API

    • Reactチームが保守
    • 将来的なReactの最適化に対応
  4. 意図が明確

    • <Activity />という名前から、「アクティブ/非アクティブの切り替え」が直感的

Activity パターンのデメリット

  1. React 19.2以降が必要

    • 古いプロジェクトでは使えない
    • アップグレードが必要
  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. まとめ

移行は簡単

  1. import { Activity } from 'react'
  2. <Activity mode='visible'/'hidden'> でラップ
  3. display: 'block'/'none' を削除

たったこれだけで、公式APIを使った実装に移行できます。

UXは同じ、DXが向上

  • ✅ ユーザー体験:完全に同じ
  • ✅ 開発者体験:副作用の自動制御で、より安全でシンプルに
  • ✅ 保守性:公式APIなので、将来的な最適化に対応

React 19.2以降なら、Activityを使おう

従来のdisplay:noneパターンも十分効果的ですが、React 19.2以降を使っているなら、公式の<Activity />コンポーネントへの移行を強く推奨します。

コードがシンプルになり、副作用の制御が自動化されることで、より安全で保守しやすい実装になります。


参考リンク


この記事が「React 19.2の新機能を知りたい」「既存のdisplay:noneパターンから移行したい」という方の参考になれば幸いです!

コメントやフィードバックがあれば、お気軽にどうぞ 👋

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?