HOWTO · React

React での展開/折りたたみ機能の段階的な実装

ユーザーがボタンをクリックしてコンテンツを展開/折りたたみできるようにする方法については、このガイドをお読みください。 これは、実際の例の段階的な内訳です。

このページの内容

ページに長いセクションや投稿の一覧がある場合、読者が表示内容を選べるようにします。Reactでは、ボタンで状態を更新し、条件付きレンダリングでその状態に応じてコンテナ全体または個々の項目を表示・非表示にできます。重要なのは、独立した表示切り替えをそれぞれ別の状態として管理することです。

Reactで展開・折りたたみ機能を実装する

1つのセクションには真偽値の状態を使い、項目ごとに独立した操作が必要な場合は、項目IDをキーにしたオブジェクトを使います。状態が更新されると、Reactは対応するJSXを再びレンダリングします。1つのセクションで使う基本形は、const [isOpen, setIsOpen] = useState(true)、onClick={() => setIsOpen(open => !open)}を設定したボタン、そして{isOpen && <section>...</section>}のような条件付きコンテンツです。関数型の更新処理は最新の値を参照し、値がfalseの場合、&&式は何もレンダリングしません。

次のプレーンなJavaScriptモデルでは、IDを独立した表示状態に対応付ける方法を示します。これは単体で動作するモデルであり、Reactの画面はレンダリングしません。

次のJavaScriptモデルを実行します。

このセクションのコードと表示されるラベルや出力は英語のままです。

const postIds = ["post1", "post2", "post3"];
const visible = Object.fromEntries(postIds.map((id) => [id, true]));

visible.post2 = !visible.post2;
console.log(postIds.map((id) => `${id}: ${visible[id]}`).join(", "));
post1: true, post2: false, post3: true

これらの操作をまとめたReactコンポーネントを示します。アプリケーションのモジュールでは、reactから* as ReactとuseStateをインポートします。この例ではcreateElementを使っているため、JavaScriptとしてもReactのコードとしても有効です。提供された実行証拠が対象としているのはプレーンなJavaScriptモデルであり、Reactのレンダリングではありません。そのため、このコンポーネントは説明用で、上の実行可能なJavaScriptモデルとは別の例です。

function PostList({ posts }) {
  const postIds = posts.map((post) => post.id);
  const [sectionOpen, setSectionOpen] = useState(true);
  const [visible, setVisible] = useState(() =>
    Object.fromEntries(postIds.map((id) => [id, true]))
  );

  function togglePost(id) {
    setVisible((current) => ({ ...current, [id]: !current[id] }));
  }

  function setAllVisible(value) {
    setVisible(Object.fromEntries(postIds.map((id) => [id, value])));
  }

  return React.createElement(
    "section",
    null,
    React.createElement(
      "button",
      { onClick: () => setSectionOpen((open) => !open) },
      sectionOpen ? "Hide posts" : "Show posts"
    ),
    React.createElement(
      "button",
      { onClick: () => setAllVisible(true) },
      "Show All"
    ),
    React.createElement(
      "button",
      { onClick: () => setAllVisible(false) },
      "Hide All"
    ),
    sectionOpen &&
      React.createElement(
        "div",
        null,
        posts.map((post) =>
          React.createElement(
            "article",
            { key: post.id },
            React.createElement(
              "button",
              { onClick: () => togglePost(post.id) },
              visible[post.id] ? "Collapse" : "Expand"
            ),
            visible[post.id] &&
              React.createElement("p", null, post.text)
          )
        )
      )
  );
}

visibleオブジェクトは、各投稿IDをそれぞれの真偽値に対応付けます。投稿のボタンをクリックすると、その項目だけが変わります。currentをスプレッドすると、ほかの投稿の値はそのまま保たれます。親リストの表示状態は、別のsectionOpenで管理します。親リストを閉じると、表示中の子要素はレンダリングされなくなりますが、各項目の値はリセットされないため、再び開くと以前と同じ投稿が表示されます。一括操作のボタンは、すべての項目を同じ真偽値に設定します。親が閉じているときに「すべて表示」を押しても各項目の値は更新されますが、親を開くまでは投稿の内容は表示されません。

Reactでコンテナ全体を展開・折りたたむ

親セクションには独自の表示状態を持たせます。ボタンでその値を切り替え、たとえば{sectionOpen && <div>{/* list */}</div>}のように条件付きレンダリングを使って、親の中にあるリスト全体を表示します。親が非表示になると、各項目の表示値がtrueのままでも、子要素はどれもレンダリングされません。親を再び表示すると、表示状態がtrueの項目がレンダリングされます。

状態がどこにあるかを区別することが大切です。親を非表示にしても、親が保持する各項目の表示値は残ります。一方、条件付きレンダリングで子コンポーネントがアンマウントされると、そのコンポーネント内のローカル状態は破棄されます。上の例では表示値をPostListが保持しているため、セクションを再び開くと同じ項目が表示されます。親を閉じたときにすべての項目もリセットしたい場合は、同じイベントハンドラー内で各項目の表示値を更新します。そうでなければ、親と項目の操作を独立させておきます。CodeSandboxの例は、元の記事にある操作可能なデモです。

Reactで特定の投稿を展開・折りたたむ

各項目のIDを使って、その項目自身の表示値だけを取得・更新します。状態を更新するときは、既存のオブジェクトを変更せず、新しいオブジェクトを作成します。次の値が前の状態に依存する場合は、関数型の更新が便利です。setVisible(current => ({ ...current, [id]: !current[id] }))。計算されたプロパティ[id]は、クリックされた項目の値を選び、スプレッド構文はほかの項目の値を保ちます。

リスト内のボタンから項目ごとにsetVisibleを呼び出し、{visible[id] && <Post post={post} />}のような条件式でその項目の内容を表示できます。リストの内容が変わる可能性がある場合は、現在のIDから初期表示状態のオブジェクトを作り、データ変更時に新しいIDや削除されたIDも処理します。キーを手書きした固定のオブジェクトでは、記載した項目しか扱えません。

Reactですべての投稿を展開または折りたたむ

一括操作では、すべての項目を同じ真偽値に設定します。リストと表示状態が一致するよう、IDからオブジェクトを作成します。

const setAllVisible = (value) => setVisible(Object.fromEntries(postIds.map(id => [id, value])))

「すべて折りたたむ」にはonClick={() => setAllVisible(false)}を、「すべて表示」にはonClick={() => setAllVisible(true)}を使います。これらの操作で項目の表示値が更新されます。親コンテナが非表示の場合、「すべて表示」を押すと各項目の状態は変わりますが、親を表示するまでは内容も見えません。

まとめ

1つのセクションには真偽値を1つ使い、項目ごとの操作にはIDと真偽値の対応付けを使います。親の表示状態、各項目の表示状態、一括操作を状態として明確に分けておけば、それぞれのボタンが意図した部分を変更します。ボタンの見た目はスタイルで変えられますが、読者に何を表示するかは状態と条件付きレンダリングによって決まります。