【React】非制御コンポーネントはいつ使うのか

4 min読了の目安(約3900字TECH技術記事

はじめに

今回は、Reactにおける非制御コンポーネント(uncontrolled component)についてご紹介したいと思います。
主にはDOMをデータソースとすることで、React以外のコードに値を渡すために使われます。

非制御コンポーネントとは?

Reactの公式ドキュメントに以下のページがあります。

ページ中には以下のような説明があります。

ほとんどの場合では、フォームの実装には制御されたコンポーネント (controlled component) を使用することをお勧めしています。制御されたコンポーネントでは、フォームのデータは React コンポーネントが扱います。非制御コンポーネント (uncontrolled component) はその代替となるものであり、フォームデータを DOM 自身が扱います。

非制御コンポーネントを記述するには、各 state の更新に対してイベントハンドラを書く代わりに、ref を使用して DOM からフォームの値を取得します。

ref自体は外部ライブラリやフォーカス・スクロールなど一部のイベント操作のサンプルコード中で利用されているのをしばしば見かけるため、どういったものか理解されている方も多いと思います。

非制御コンポーネントでは、本来stateで持つようなデータをrefを用いてDOM内で保持しています。

実装例

以下に実装例を示します。
SomeTextInput.tsxというTypescriptで記載された、inputbuttonを持つオーソドックスなコンポーネントを想定します。

制御コンポーネント

まずは通常通りの書き方です。
下記のように状態をコンポーネント内で保持しているコンポーネントのことを一般に制御コンポーネントと呼びます。

SomeTextInput.tsx
import React from "react";

interface SomeProps {}

interface SomeState {
	text: string;
}

class SomeTextInput extends React.Component<SomeProps, SomeState> {
	/**
	 * コンストラクタ
	 * @param props 
	 */
	constructor(props: SomeProps) {
		super(props);
		// State初期化
		this.state = {
			text: "",
		};
	}

	/**
	 * Input変更時処理
	 * @param event 
	 */
	onChangeText = (event: React.ChangeEvent<HTMLInputElement>) => {
		this.setState({
			text: event.target.value,
		});
	};

	/**
	 * OKボタン押下時処理
	 */
	onClickButton = () => {
		const { text } = this.state;
		console.log(text);
	};

	render = () => {
		const { text } = this.state;
		return (
			<div className="App">
				<input type="text" value={text} onChange={this.onChangeText} />
				<button onClick={this.onClickButton}>OK</button>
			</div>
		);
	};
}

export default SomeTextInput;

非制御コンポーネント

このSomeTextInput.tsxを非制御コンポーネントとして書き換えると以下のようになります。

SomeTextInput.tsx
import React from "react";

interface SomeProps {}

class SomeTextInput extends React.Component<SomeProps> {

	// ref
	private _input: React.RefObject<HTMLInputElement>;

	/**
	 * コンストラクタ
	 * @param props 
	 */
	constructor(props: SomeProps) {
		super(props);
		this._input = React.createRef<HTMLInputElement>();
	}

	/**
	 * OKボタン押下時処理
	 */
	onClickButton = () => {
		console.log(this._input.current.value);
	};

	render = () => {
		return (
			<div className="App">
				<input type="text" ref={this._input} />
				<button onClick={this.onClickButton}>OK</button>
			</div>
		);
	};
}

export default SomeTextInput;

大きくはinputのプロパティが変わっています。
onClickButton内でrefから入力値を直接参照するためstateは不要になり、それに伴いonChangeTextがなくなっています。

なぜDOMで保持する?

非制御コンポーネントの最大の利点は「React以外のコードに値を簡単に渡せる」という点です。
例えば入力フォームだけをReactのコードで記載しており、それ以外の箇所を別なフレームワークで管理している場合に、DOM内に値があることでReactでないコードからも簡単に値にアクセスすることができます。

また、サンプルを見ても分かりますがstateに関する記述がなくなるため、僅かにコード量が減少します(といっても、これを目的に非制御コンポーネントを扱うのは推奨しません)。

非制御コンポーネントを使ってもいいケース

非制御コンポーネントのメリットは先に挙げた通りですが、それ以外の場合でも非制御コンポーネントを使っても「問題ない」とされるケースがいくつかあります。
※あくまで「使っても問題がない」だけで、推奨するわけではありません。

公式ドキュメント中に以下記事のリンクがありました。

英文記事ですが、これによると以下のケースは非制御コンポーネントでも問題ないようです。

  • submitのようにフォーム内容を一回で送信する場合
  • 送信時にフォームの各値のvalidationを行う場合

これはなぜかというと制御コンポーネントと非制御コンポーネントの性質に由来します。
制御コンポーネントは状態をstate(ないしstore)で持ち、値の入力の度にonChangeXXXが呼び出され、都度状態を更新していきます。
つまり、状態と画面表示が完全に同期しています。

それに対して非制御コンポーネントは、状態をDOMが持っているだけのため、仮にその内容を画面に反映しようとする場合は能動的にDOMから値を取得する必要があります。
コード中のthis._input.current.valueのようにDOMから一度値を吸い出して、さらにそれを画面に反映させるコードを書く必要がある、というイメージです。

従って、下記の例の場合は非制御コンポーネントの方がかえって記述量や管理コストが嵩んでしまうため非推奨となります。

  • 同期的にvalidationを行う場合
  • 入力状態によってボタンのdisableなど、画面表示を制御する場合

他にもいくつか非推奨の例はありますが、主なところだと上記のものがあり、最近のものでいうと大概のUIは上記の作りになっているため、基本的には制御コンポーネントを使った方がよいという結論になります。

まとめ

今回は非制御コンポーネントについて、実例とメリット・使わない方がいい場合をサンプルコードを交えて紹介しました。

記事によっては「非制御コンポーネントの方が高速」といった主張も見かけます(stateが変化しないので、再描画がかかっていないため?)ですが、真偽が不明だったので今回は扱いませんでした。

やはり主にはReact以外のコードとの接続のために使われることが多いと思うので、その際の参考になれば幸いです。