React 19.2の<Activity>を19.1以下のバージョンで使用する方法

<Activity>とは

<Activity>はReact 19.2で追加された条件付きでレンダリングするための新機能です。

以下のように、reactからActivityをimportして、Activityのmodeに 'visible' を渡すと表示、'hidden' を渡すと非表示になります。

React 19.2 <Activity>

※ hidden以外が渡された場合も表示されますが、型エラーになります。

App.tsx
import { Activity } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const currentSeconds = new Date().getSeconds()
  const currentMode = currentSeconds % 2 === 0 ? 'visible' : 'hidden'

  return (
    <>
      <h1>Activity</h1>
      <Activity mode={currentMode}>
        <div>
          <a href="https://vite.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
          <a href="https://react.dev" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
        </div>
      </Activity>
      <h2>偶数秒ならロゴ表示({currentSeconds})</h2>
      <button onClick={() => location.reload()}>ページをリロード</button>
    </>
  )
}

export default App

React 19.2 Activity Sample

一般的に、React 19.1までは {isShow && <Page />} のような形で表示・非表示を行うことが多いです。

しかし、<Activity>で囲んだほうが、表示・非表示の箇所がわかりやすくなり、追加・削除によるGitの差分も明確になるメリットがあります。

TSX
{isShow && <Page />}
TSX
<Activity mode={isShow ? 'visible' : 'hidden'}>
  <Page />
</Activity>

<Activity>のmodeは 'visible' または 'hidden' のどちらかしか受け取らないので、複数箇所に使用する場合はgetMode()の関数を使用して、'visible' または 'hidden' を返すようにするとスマートです。

TSX
import { Activity } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const currentSeconds = new Date().getSeconds()
  const isEvenSeconds = currentSeconds % 2 === 0
  const getMode = (mode: boolean) => mode ? 'visible' : 'hidden'

  return (
    <>
      <h1>Activity</h1>
      <Activity mode={getMode(isEvenSeconds)}>
        <div>
          <a href="https://vite.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
          <a href="https://react.dev" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
        </div>
      </Activity>
      <h2>秒が偶数ならロゴ表示({new Date().getSeconds()})</h2>
      <button onClick={() => location.reload()}>ページをリロード</button>
    </>
  )
}

export default App

React 19.1以下で<Activity>を使用する方法

<Activity>は使ってみると便利なので、React 19.1以下のバージョンでも使ってみたいと思う方もいると思います。

React 19.1以下で使いたい場合はActivity.tsxを以下のようなコードで作成して、条件を満たした場合に、children(囲んだコード)を返すようにすれば実装できます。

Activity.tsx
import React, { useState, useEffect, useRef, cloneElement } from 'react'

type RegisterCleanupProp = { __registerCleanup?: (fn: () => void) => void }

export function Activity({
  mode,
  children,
}: {
  mode: 'visible' | 'hidden'
  children: React.ReactElement<RegisterCleanupProp>
}) {
  const [key, setKey] = useState(0)
  const cleanupRef = useRef<(() => void) | null>(null)

  useEffect(() => {
    if (mode === 'hidden') {
      cleanupRef.current?.()
      cleanupRef.current = null
    } else {
      setKey((k) => k + 1)
    }
  }, [mode])

  return (
    <div style={{ display: mode === 'visible' ? undefined : 'none' }}>
      {cloneElement(children, {
        key,
        __registerCleanup: (fn: () => void) => {
          cleanupRef.current = fn
        },
      })}
    </div>
  )
}
App.tsx
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import { Activity } from './Activity'
import './App.css'

function App() {
  const currentMode = new Date().getSeconds() % 2 === 0 ? 'visible' : 'hidden'

  return (
    <>
      <h1>Activity</h1>
      <Activity mode={currentMode}>
        <div>
          <a href="https://vite.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
          <a href="https://react.dev" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
        </div>
      </Activity>
      <h2>秒が偶数ならロゴ表示({new Date().getSeconds()})</h2>
      <button onClick={() => location.reload()}>ページをリロード</button>
    </>
  )
}

export default App

React 19.1 Activity Sample