React公式サイトのドキュメントが2023年3月16日に改訂されました(「Introducing react.dev」参照)。本稿は、基本解説の「Sharing State Between Components」をかいつまんでまとめた記事です。ただし、コードにはTypeScriptを加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。
なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。
ふたつのコンポーネントの状態を、つねに一緒に変化させたい場合があるでしょう。そのためには、状態をそれぞれのコンポーネントにもたせるのでなく、もっとも近い共通の親に移すべきです。そのうえで、親から子には値をプロパティで渡します。これは状態の引き上げ(リフトアップ)と呼ばれ、Reactのコードでよく用いられるやり方です。
状態を引き上げる例
ふたつの別々のパネル(Panel
)が子コンポーネントとして開く、アコーディオン(Accordion
)のコンポーネントをつくってみましょう。
-
Accordion
Panel
Panel
Panel
コンポーネントにはそれぞれブール値の状態変数isActive
が備わり、コンテンツをパネルに表示するかどうか決まります。[Show]ボタンを押すとパネルが開くでしょう(サンプル001)。
type Props = { title: string; children: ReactNode };
export const Panel: FC<Props> = ({ title, children }) => {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>Show</button>
)}
</section>
);
};
export default function Accordion() {
return (
<>
<Panel title="About">
...[略]...
</Panel>
<Panel title="Etymology">
...[略]...
</Panel>
</>
);
}
サンプル001■React + TypeScript: Sharing State Between Components 01
ボタンはもう一方のパネルには影響しません。状態変数isActive
はそれぞれのコンポーネントがもち、互いに独立しているからです。
-
Accordion
-
Panel
isActive
-
Panel
isActive
-
さてここで、つねにひとつのパネルだけが開いているように変更しましょう。すると、ひとつを開いたら、もうひとつはたたまなければなりません。そのとき用いるのが、親コンポーネントへの状態の引き上げです。つぎの3つの手順で進めます。
- 状態は子コンポーネントから除いてください。
- データは共通の親コンポーネントから渡します。
- 共通の親コンポーネントに状態を加え、イベントハンドラとともに子に与えましょう。
こうして、Accordion
コンポーネントはふたつのパネルを連携し、つねにひとつだけ開けるようになるのです。
手順1: 状態を子コンポーネントから除く
Panel
のisActive
の制御は、親コンポーネント(Accordion
)に委ねましょう。つまり、isActive
は親コンポーネントが、子のPanel
にプロパティとして与えるのです。Panel
コンポーネントの状態は削り、親から引数で受け取るプロパティにisActive
を加えます。
// export const Panel: FC<Props> = ({ title, children }) => {
export const Panel: FC<Props> = ({ title, children, isActive }) => {
// const [isActive, setIsActive] = useState(false);
};
Panel
コンポーネントはisActive
を変更できなくなり、親が共通に渡すプロパティ値にしたがうようになりました。
手順2: データを共通の親コンポーネントから渡す
状態の引き上げのためには、連携したいコンポーネントにもっとも近い共通の親を探し出さなければなりません。今回の例では単純にAccordion
コンポーネントです。連携させるPanel
に共通のプロパティ値が与えられるので、「信頼できる情報源」(source of truth)になります。
-
Accordion
(もっとも近い共通の親)-
<Panel isActive={value}>
const Panel = ({ isActive }) => {}
const Panel = ({ isActive }) => {}
-
Accordion
コンポーネントから子のPanel
にプロパティisActive
を渡すように書き替えたのがつぎのコードです。もっとも、値を固定のtrue
にしましたので、パネルは閉じません。また、今はPanel
のonClick
イベントハンドラも使えなくなりました。
export default function Accordion() {
return (
<>
<Panel title="About" isActive={true}>
...[略]...
</Panel>
<Panel title="Etymology" isActive={true}>
...[略]...
</Panel>
</>
);
}
type Props = { title: string; children: ReactNode; isActive: boolean };
export const Panel: FC<Props> = ({ title, children, isActive }) => {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button>Show</button>
)}
</section>
);
};
手順3: 共通の親コンポーネントに状態を加える
状態を引き上げると、もともともっていた状態と性質が変わることも少なくありません。一度に開くのはひとつのパネルとするには、共通の親であるAccordion
コンポーネントはどのパネルがアクティブなのかを知っているべきでしょう。そこで、親コンポーネントの状態は、ブール値でなく、開いているパネルを示す数値インデックスとします。
activeIndex
の初期値は0なので、はじめに開くのは上のパネルです。パネルの[Show]ボタンをクリックするたびに、状態変数値は切り替えなければなりません。けれど、子コンポーネントPanel
は、親がもつ状態変数を直に変えることは禁じられます。そこで、Accordion
コンポーネントから子のPanel
にonShow
「イベントハンドラをプロパティとして渡す」ことにより変更が許されるようになるのです(サンプル002)。
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
{/* <Panel title="About" isActive={true}> */}
<Panel
title="About"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...[略]...
</Panel>
{/* <Panel title="Etymology" isActive={true}> */}
<Panel
title="Etymology"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...[略]...
</Panel>
</>
);
}
// type Props = { title: string; children: ReactNode; isActive: boolean };
type Props = {
title: string;
children: ReactNode;
isActive: boolean;
onShow: () => void;
};
// export const Panel: FC<Props> = ({ title, children, isActive }) => {
export const Panel: FC<Props> = ({ title, children, isActive, onShow }) => {
return (
<section className="panel">
{isActive ? (
<p>{children}</p>
) : (
// <button>
<button onClick={onShow}>Show</button>
)}
</section>
);
};
サンプル002React + TypeScript: Sharing State Between Components 02
状態を子コンポーネントにもっとも近い共通の親に引き上げました。すると、共通の状態にもとづいて、子コンポーネントが連携させられるのです。
- 親コンポーネント
Accordion
は状態activeIndex
をもちます。- 子コンポーネント
Panel
に渡されるisActive
は、その状態から算出されたプロパティです。 - 子コンポーネント
Panel
にはさらに、状態変数activeIndex
を設定するイベントハンドラonShow
も渡しました。
- 子コンポーネント
- 子コンポーネント
Panel
は親からプロパティとして、isActive
とonShow
を受け取ります。- 開くべきパネルを統一的に定めるのが
isActive
です。 - ボタンで切り替えるパネルの開閉は、与えられたイベントハンドラ
onShow
により、状態変更そのものは親に委ねます。
- 開くべきパネルを統一的に定めるのが
コンポーネントがコントロールされているかいないか
状態をローカルにもつコンポーネントは「コントロールされていない」と呼ぶのが一般的です。たとえば、前掲サンプル001のPanel
コンポーネントがそうでしょう。状態変数isActive
をみずからの中にもつので、親コンポーネントも状態が制御できないからです。
これに対して、コンポーネントの重要な情報が、自分のローカルな状態でなく、プロパティにより操作されている場合「コントロールされている」といえるでしょう。親コンポーネントが与えるプロパティで、ふるまいが決められるからです。前掲サンプル002のPanel
コンポーネントは、親のAccordion
から渡されるisActive
プロパティで制御されていました。
制御されていないコンポーネントは、設定があまり要りませんので、親の中で使うには簡単です。けれど、コンポーネント間で連携をとるといった柔軟性が損なわれます。制御されたコンポーネントなら柔軟性は高いものの、親コンポーネントがプロパティを細かく設定しなければならないのです。
実際のところ、「コントロールされている」とか「コントロールされていない」というのは、厳密な技術用語とはいえません。多くのコンポーネントは、ローカルな状態とプロパティをともに備えます。ただ、コンポーネントの設計や提供する機能を考えるときには役立つでしょう。コンポーネントは、どの情報を(プロパティで)コントロールするか、どの情報は(ローカルな状態にして)コントロールしないか考えてつくることが大切です。
状態の信頼できる唯一の情報源
Reactアプリケーションの中で、コンポーネントはそれぞれの状態をもちます。ある状態はツリーの下層のコンポーネントに属するかもしれません。アプリケーションの最上層に備わる状態もあるでしょう。たとえば、クライアントサイドのルーティングライブラリは、現行ルートをReactの状態に実装します。そのうえで、プロパティとして下層に渡すのです。
個別の状態ごとに、どのコンポーネントにそれを「保持」させるか選ばなければなりません。「信頼できる唯一の情報源」と呼ばれる原則です。すべての状態をひとつの場所にまとめなければならないということではありません。けれど、特定の状態ごとに、その情報を備えるコンポーネントがあるということです。コンポーネント間で共有する状態は重複させるのでなく、それらの共通する親に引き上げ、必要とする子にはプロパティで渡しましょう。作業が進んでアプリケーションが変化したら、状態のもたせ方は再検討すればよいのです。
まとめ
この記事では、つぎのような項目についてご説明しました。
- ふたつのコンポーネントを連携したいときは、それらが用いる状態は共通の親に引き上げましょう。
- 共通の親コンポーネントに引き上げた状態から算出した連携に必要な情報は、子にプロパティとして渡します。
- 親コンポーネントの状態を子から変更するには、与えるプロパティにイベントハンドラも含めてください。
- コンポーネントを(プロパティで)「コントロールされている」か(ローカルな状態で)「コントロールされていないか」考えることは有用です。