<Activity> - This feature is available in the latest Experimental version of React

Experimental Feature

この API は実験的なものであり、まだ React の安定版では利用できません

React パッケージを最新の実験的バージョンにアップグレードすることで試すことができます。

  • react@experimental
  • react-dom@experimental
  • eslint-plugin-react-hooks@experimental

React の実験的バージョンにはバグが含まれている可能性があります。本番環境では使用しないでください。

<Activity> を使い、UI の一部を非表示にしたり表示したりします。

<Activity mode={mode}>
<Page />
</Activity>

リファレンス

<Activity>

UI の一部を <Activity> でラップすることで、その可視性状態を管理します。

import {unstable_Activity as Activity} from 'react';

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

“hidden” の場合、<Activity />children はページに表示されません。新しい <Activity> が “hidden” としてマウントされると、ページ上の表示されているコンテンツをブロックすることなく、低優先度でコンテンツをプリレンダー (pre-render) しますが、エフェクトを作成することによるマウントは行いません。“visible” の Activity が “hidden” に切り替わると、概念的にはすべてのエフェクトを破棄することでアンマウントしますが、その state は保存します。これにより、“hidden” の Activity の state を再作成することなく、“visible” と “hidden” の state を高速に切り替えることができます。

将来的には、“hidden” の Activity はメモリなどのリソースに基づいて state を自動的に破棄する可能性があります。

props

  • children: 実際にレンダーしたい UI。
  • 省略可能 mode: “visible” または “hidden”。デフォルトは “visible”。“hidden” の場合、子の更新は低優先度として遅延される。Activity が “visible” に切り替わるまで、コンポーネントはエフェクトを作成しない。“visible” の Activity が “hidden” に切り替わると、エフェクトは破棄される。

注意点

  • hidden の間、<Activity>children はページ上で非表示になります。
  • <Activity> は、“visible” から “hidden” に切り替わる際、React の state や DOM の状態を破棄することなくすべてのエフェクトをアンマウントします。これは、マウント時に一度だけ実行されることが期待されるエフェクトであっても、“hidden” から “visible” に切り替わる際に再度実行されることを意味します。概念的には、“hidden” 状態の Activity はアンマウントされるが破棄されてもいないということです。この挙動による予期せぬ副作用をキャッチするために <StrictMode> を使用することをお勧めします。
  • <ViewTransition> と共に使用すると、トランジションで表示される非表示の Activity は “enter” アニメーションを起動します。トランジションで非表示になる表示中の Activity は “exit” アニメーションを起動します。
  • <Activity mode="hidden"> でラップされた UI は、SSR のレスポンスに含まれません。
  • <Activity mode="visible"> でラップされた UI は、他のコンテンツよりも低い優先度でハイドレーションされます。

使用法

UI の一部を事前レンダーする

<Activity mode="hidden"> を使用して、UI の一部を事前レンダーしておけます。

<Activity mode={tab === "posts" ? "visible" : "hidden"}>
<PostsTab />
</Activity>

Activity が mode="hidden" でレンダーされると、children はページに表示されませんが、ページ上の表示されているコンテンツよりも低い優先度でレンダーされます。

後で mode が “visible” に切り替わると、事前レンダーされた子要素がマウントされ、表示されるようになります。これは、ユーザが次に操作する可能性が高い UI を準備して、読み込み時間を短縮するために使用できます。

以下の useTransition の例では、PostsTab コンポーネントが use を使用してデータをフェッチしています。“Posts” タブをクリックすると、PostsTab コンポーネントがサスペンドし、ボタンにローディング中という状態が表示されます。

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}

この例の場合、“Posts” タブをクリックした際、ユーザは投稿が読み込まれるのを待つ必要があります。

非アクティブなタブを非表示の <Activity> で事前レンダーしておくことで、“Posts” タブの遅延を減らすことができます。

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


UI の state を保持する

<Activity> を “visible” から “hidden” に切り替える際に、当該部分の UI の state を保持できます。

<Activity mode={tab === "posts" ? "visible" : "hidden"}>
<PostsTab />
</Activity>

Activity が mode="visible" から “hidden” に切り替わると、children はページ上で非表示になり、すべてのエフェクトを破棄することでアンマウントしますが、React の state と DOM の状態は保持します。

後で mode が “visible” に切り替わると、保存された state は、エフェクトを作成して子をマウントする際に再利用されます。これは、ユーザが再度操作する可能性が高い UI の state を保持し、DOM や React の state を維持するために使用できます。

useTransition の以下の例では、ContactTab に送信するメッセージの下書きを含む <textarea> が含まれています。テキストを入力して別のタブに移動し、その後 “Contact” タブを再度クリックすると、下書きメッセージは失われてしまいます。

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}

つまりユーザが入力した DOM の state が失われてしまっています。非アクティブなタブを <Activity> を使って非表示にすることで、Contact タブの state を保持できます。

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


トラブルシューティング

Activity が非表示のときにエフェクトがマウントされない

<Activity> が “hidden” の場合、すべてのエフェクトはアンマウントされます。概念的には、コンポーネントはアンマウントされていますが、React は後で使用するために state を保存しています。

これは Activity の機能です。なぜなら、UI の非表示部分に対してサブスクリプションが登録されなくなり、非表示コンテンツの作業量が削減されるからです。また、ビデオの一時停止のようなクリーンアップ(Activity なしでアンマウントした場合に期待される動作)が実行されることも意味します。Activity が “visible” に切り替わると、エフェクトが作成されマウントが起き、それによりイベントハンドラの登録やビデオの再生が起こります。

ボタンごとに異なるビデオが再生される、以下の例を考えてみましょう。

import { useState, useRef, useEffect } from 'react';
import './checker.js';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    const videoRef = ref.current;
    videoRef.play();
    
    return () => {
      videoRef.pause();
    }
  }, []);

  return <video ref={ref} src={src} muted loop playsInline/>;
}

export default function App() {
  const [video, setVideo] = useState(1);
  return (
    <>
      <div>
        <button onClick={() => setVideo(1)}>Big Buck Bunny</button>
        <button onClick={() => setVideo(2)}>Elephants Dream</button>
      </div>
      {video === 1 &&
        <VideoPlayer key={1}
          // 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org
          src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4" />

      }
      {video === 2 && 
        <VideoPlayer key={2}
          // 'Elephants Dream' by Orange Open Movie Project Studio, licensed under CC-3.0, hosted by archive.org
          src="https://archive.org/download/ElephantsDream/ed_1024_512kb.mp4"
        />
      }
    </>
  );
}

ビデオを切り替えて戻ってくると、そのビデオが最初から再読み込みされてしまっています。state を維持するために、両方のビデオをレンダーしておき、非アクティブなビデオを display: none で非表示にすればいいと思うかもしれません。しかし、これにより両方のビデオが同時に再生されてしまいます。

import { useState, useRef, useEffect } from 'react';
import VideoChecker from './checker.js';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    const videoRef = ref.current;
    videoRef.play();
    
    return () => {
      videoRef.pause();
    }
  }, []);

  return <video ref={ref} src={src} muted loop playsInline/>;
}

export default function App() {
  const [video, setVideo] = useState(1);
  return (
    <>
      <div>
        <button onClick={() => setVideo(1)}>Big Buck Bunny</button>
        <button onClick={() => setVideo(2)}>Elephants Dream</button>
      </div>
      <div style={{display: video === 1 ? 'block' : 'none'}}>
        <VideoPlayer
          // 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org
          src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4" />

      </div>
      <div style={{display: video === 2 ? 'block' : 'none'}}>
        <VideoPlayer
          // 'Elephants Dream' by Orange Open Movie Project Studio, licensed under CC-3.0, hosted by archive.org
          src="https://archive.org/download/ElephantsDream/ed_1024_512kb.mp4"
        />
      </div>
      <VideoChecker />
    </>
  );
}

Activity が非表示のときにエフェクトをマウントしてしまえば、これと似たことが起きてしまうのです。同様に、Activity が非表示になるときにエフェクトをアンマウントしない場合、ビデオはバックグラウンドで再生され続けてしまいます。

Activity は、最初に “hidden” 状態でレンダーされたときにはエフェクトを作成せず、“visible” から “hidden” に切り替えるときにもすべてのエフェクトを破棄することで、この問題を解決します。

import { useState, useRef, useEffect, unstable_Activity as Activity } from 'react';
import VideoChecker from './checker.js';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    const videoRef = ref.current;
    videoRef.play();
    
    return () => {
      videoRef.pause();
    }
  }, []);

  return <video ref={ref} src={src} muted loop playsInline/>;
}

export default function App() {
  const [video, setVideo] = useState(1);
  return (
    <>
      <div>
        <button onClick={() => setVideo(1)}>Big Buck Bunny</button>
        <button onClick={() => setVideo(2)}>Elephants Dream</button>
      </div>
      <Activity mode={video === 1 ? 'visible' : 'hidden'}>
        <VideoPlayer
          // 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org
          src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4" />
      </Activity>
      <Activity mode={video === 2 ? 'visible' : 'hidden'}>
        <VideoPlayer
          // 'Elephants Dream' by Orange Open Movie Project Studio, licensed under CC-3.0, hosted by archive.org
          src="https://archive.org/download/ElephantsDream/ed_1024_512kb.mp4"
        />
      </Activity>
      <VideoChecker />
    </>
  );
}

このため、最善の考え方は、Activity は概念的にはコンポーネントを「アンマウント」および「再マウント」するが、React の state や DOM の状態を後のために保持しておく、と考えることです。実際、そのエフェクトは不要かものガイドに従っている限り、これは期待どおりに機能します。問題のあるエフェクトを積極的に見つけるに、<StrictMode> を追加することをお勧めします。これにより、Activity のアンマウントとマウントが積極的に実行され、予期せぬ副作用をキャッチできます。

非表示の Activity が SSR でレンダーされない

サーバサイドレンダリング中に <Activity mode="hidden"> を使用すると、Activity のコンテンツは SSR レスポンスに含まれません。これは、コンテンツがページに表示されないので初期レンダーには必要ないためです。コンテンツを SSR レスポンスに含める必要がある場合は、useDeferredValue のような別のアプローチを使用して、コンテンツのレンダーを遅延させることができます。