はじめに
React 19.2で追加された<Activity>の理解を深めるために、FintanのSPA + REST API構成のハンズオンのTodoアプリをローカルにダウンロードし、<Activity>を組み込んでみました。
なお、ハンズオンアプリの環境構築やバージョンアップ作業に関することは記載しません。
Activityとは?
React 19.2で追加された新機能です。公式ドキュメントによると:
https://react.dev/reference/react/Activity
Activity バウンダリを用いてコンポーネントを非表示にすると、React は state を後で使うために「セーブ」しておくことができます。
通常、{isOpen && <Component />}みたいに条件分岐するとコンポーネントがアンマウントされてstateも消えます。
でも<Activity>を使えば、非表示にしてもstateを保持できます。
どう動くのか?
-
mode="hidden"の時-
display: noneで見えなくなる - useEffectのクリーンアップ関数が実行される(タイマーやAPI接続などを停止)
- でもstateは残ってる
- 新しいpropsに反応して再レンダーはされるが、優先度が低くなる
-
-
mode="visible"に戻した時- 以前のstateが復元される
- useEffectが再実行される(タイマーやAPI接続などを再開)
追加した機能について
<Activity>の機能は「非表示にしてもstateを保持する」こと。
公式ドキュメントだとサイドバーの開閉が例として出てます。
今回は学習目的で、TodoアプリにTodoの編集パネルを追加してみました。
具体的には、Todoをクリックすると右側にパネルが開いて、そこでTodoの内容を編集できる機能です。
やりたいこと:
- Todoクリックで右側にパネルを表示
- パネル内でTodoの内容を編集
- 保存せずに閉じても編集中の内容を保持
画面はこんな感じで作りました。
Activity使う前の実装
まず、右側にパネルを表示する部分まで実装してみます。
TodoBoard.tsx
export const TodoBoard: React.FC = () => {
const [selectedTodoId, setSelectedTodoId] = useState<number | null>(null);
const selectedTodo = todos.find(todo => todo.id === selectedTodoId);
return (
<div>
<TodoList onTodoClick={(id) => setSelectedTodoId(id)} />
{selectedTodo && (
<TodoDetailPanel
todo={selectedTodo}
onClose={() => setSelectedTodoId(null)}
onSave={handleDetailSave}
/>
)}
</div>
);
};
TodoDetailPanel.tsx
export const TodoDetailPanel: React.FC<Props> = ({ todo, onClose, onSave }) => {
const [editText, setEditText] = useState(todo.text);
const handleSave = () => {
onSave(todo.id, editText);
};
return (
<div className={styles.panel}>
<div className={styles.header}>
<h2>Todo詳細</h2>
<button onClick={onClose}>✕</button>
</div>
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
/>
<button onClick={handleSave}>保存</button>
</div>
);
};
何が問題?
この実装だと:
- サイドパネルでテキスト編集中
- 保存せずに✕で閉じる →
selectedTodoがnullになってコンポーネントがアンマウント - 同じTodoをもう一度開く → コンポーネントが新規マウント
- 編集してた内容が消えています
{selectedTodo && ...}の条件分岐でコンポーネントが完全に消えるので、editTextのstateも一緒に消えます。
Activity使ってみる
では<Activity>で状態を保持できるようにしてみます。
最初の実装
1つの<Activity>で全部管理してみます。
<Activity mode={selectedTodoId !== null ? "visible" : "hidden"}>
{selectedTodo && (
<TodoDetailPanel todo={selectedTodo} ... />
)}
</Activity>
あるTodoを編集して、保存せずに閉じて、再度編集パネルを開くと状態はリセットされずに表示されました。
しかし、Todo1を編集してからTodo2を開くと、Todo1の編集内容がTodo2にも表示されます。
1つのTodoDetailPanelを使い回してるので、editTextのstateもTodo間で共有されちゃいます。
今回は複数のTodoを編集した際に、それぞれの状態を保持するようにしてみます。
Todo毎にActivityを作る
Todo毎にActivityを作ることにします。
この方法だとTodo分のコンポーネントが常にマウントされるため、パフォーマンスへの懸念があります。
他の実装案もありますが、今回は気にせず実装します。
TodoBoard.tsx(Activity使った後)
export const TodoBoard: React.FC = () => {
const [selectedTodoId, setSelectedTodoId] = useState<number | null>(null);
return (
<div>
<TodoList onTodoClick={(id) => setSelectedTodoId(id)} />
{todos.map(todo => (
<Activity
key={todo.id}
mode={selectedTodoId === todo.id ? "visible" : "hidden"}
>
<TodoDetailPanel
todo={todo}
onClose={() => setSelectedTodoId(null)}
onSave={handleDetailSave}
/>
</Activity>
))}
</div>
);
};
これでどう変わった?
ちゃんと動くようになりました:
-
Todo1を編集中に閉じる → Todo1の
<Activity>がmode="hidden"になってeditTextが保持される -
Todo2を開く → Todo2の
<Activity>がmode="visible"になる(Todo1とは別) -
Todo1をもう一度開く →
mode="visible"に戻って編集途中の内容が復元される
各Todoが独立してるので、複数のTodoを編集中でもそれぞれの内容が個別に保持されます。
まとめ
React 19.2の<Activity>を実際に使ってみました。
使う前と使った後で動作がどう変わるか、ざっくり確認できました。
