1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React + TypeScript: コンポーネント間で状態を共有する

Last updated at Posted at 2023-04-21

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)。

src/Panel.tsx
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>
	);
};
src/App.tsx
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: 状態を子コンポーネントから除く

PanelisActiveの制御は、親コンポーネント(Accordion)に委ねましょう。つまり、isActiveは親コンポーネントが、子のPanelにプロパティとして与えるのです。Panelコンポーネントの状態は削り、親から引数で受け取るプロパティにisActiveを加えます。

src/Panel.tsx
// 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にしましたので、パネルは閉じません。また、今はPanelonClickイベントハンドラも使えなくなりました。

src/App.tsx
export default function Accordion() {
	return (
		<>

			<Panel title="About" isActive={true}>
				...[略]...
			</Panel>
			<Panel title="Etymology" isActive={true}>
				...[略]...
			</Panel>
		</>
	);
}
src/Panel.tsx
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コンポーネントから子のPanelonShowイベントハンドラをプロパティとして渡す」ことにより変更が許されるようになるのです(サンプル002)。

src/App.tsx
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>
		</>
	);
}
src/Panel.tsx
// 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は親からプロパティとして、isActiveonShowを受け取ります。
    • 開くべきパネルを統一的に定めるのがisActiveです。
    • ボタンで切り替えるパネルの開閉は、与えられたイベントハンドラonShowにより、状態変更そのものは親に委ねます。

コンポーネントがコントロールされているかいないか

状態をローカルにもつコンポーネントは「コントロールされていない」と呼ぶのが一般的です。たとえば、前掲サンプル001のPanelコンポーネントがそうでしょう。状態変数isActiveをみずからの中にもつので、親コンポーネントも状態が制御できないからです。

これに対して、コンポーネントの重要な情報が、自分のローカルな状態でなく、プロパティにより操作されている場合「コントロールされている」といえるでしょう。親コンポーネントが与えるプロパティで、ふるまいが決められるからです。前掲サンプル002のPanelコンポーネントは、親のAccordionから渡されるisActiveプロパティで制御されていました。

制御されていないコンポーネントは、設定があまり要りませんので、親の中で使うには簡単です。けれど、コンポーネント間で連携をとるといった柔軟性が損なわれます。制御されたコンポーネントなら柔軟性は高いものの、親コンポーネントがプロパティを細かく設定しなければならないのです。

実際のところ、「コントロールされている」とか「コントロールされていない」というのは、厳密な技術用語とはいえません。多くのコンポーネントは、ローカルな状態とプロパティをともに備えます。ただ、コンポーネントの設計や提供する機能を考えるときには役立つでしょう。コンポーネントは、どの情報を(プロパティで)コントロールするか、どの情報は(ローカルな状態にして)コントロールしないか考えてつくることが大切です。

状態の信頼できる唯一の情報源

Reactアプリケーションの中で、コンポーネントはそれぞれの状態をもちます。ある状態はツリーの下層のコンポーネントに属するかもしれません。アプリケーションの最上層に備わる状態もあるでしょう。たとえば、クライアントサイドのルーティングライブラリは、現行ルートをReactの状態に実装します。そのうえで、プロパティとして下層に渡すのです。

個別の状態ごとに、どのコンポーネントにそれを「保持」させるか選ばなければなりません。「信頼できる唯一の情報源」と呼ばれる原則です。すべての状態をひとつの場所にまとめなければならないということではありません。けれど、特定の状態ごとに、その情報を備えるコンポーネントがあるということです。コンポーネント間で共有する状態は重複させるのでなく、それらの共通する親に引き上げ、必要とする子にはプロパティで渡しましょう。作業が進んでアプリケーションが変化したら、状態のもたせ方は再検討すればよいのです。

まとめ

この記事では、つぎのような項目についてご説明しました。

  • ふたつのコンポーネントを連携したいときは、それらが用いる状態は共通の親に引き上げましょう。
  • 共通の親コンポーネントに引き上げた状態から算出した連携に必要な情報は、子にプロパティとして渡します。
  • 親コンポーネントの状態を子から変更するには、与えるプロパティにイベントハンドラも含めてください。
  • コンポーネントを(プロパティで)「コントロールされている」か(ローカルな状態で)「コントロールされていないか」考えることは有用です。
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?