【React】入力欄のコンポーネントについて考える。stateは自身でもつ?親で持つ?

こんにちは。

早いもので、今年ももう2月に入りましたね。
おはぎが食べたくて「自分で作ろうかな〜」と思ってレシピを読んでみたら、けっこう大変そうで尻込みをしているはこだてたろうです。
今日明日と肌寒いようなのでお体にはお気をつけください。

さて、入力欄のコンポーネントって奥が深いと思いませんか?

  • state を自身に持たせるか、親から渡すか
  • バリデーションを自身でやるか、親でやるか、親からバリデーションロジックを渡すか
  • そもそもバリデーションはどのタイミングでやるか(onChange?onSubmit?onBlur?)
  • 入力欄のほかにエラーメッセージ表示も含めたら単一責任の原則に違反するのでは?

などなど、色々と考えるべきことがあります。

この記事では、「良い入力欄のコンポーネント」について考えてみたいと思います。

目次

stateは自身で持つ?親で持つ?

考えるべきことはいくつかあるのですが、今回は「stateは自身で持つべきか、親で持つべきか?」について考えてみたいと思います。

以下のような入力フォームがあるとします。

import {useState} from "react";

function InputForm() {
  const [name, setName] = useState('')
  const [nameKana, setNameKana] = useState('')
  return (
    <div>
      <form>
        <div>
          <h2>名前</h2>
          <input
            name={'name'}
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="田中 太郎"
          />
        </div>
        <div>
          <h2>名前(カナ)</h2>
          <input
            name={'nameKana'}
            type="text"
            value={nameKana}
            onChange={(e) => setNameKana(e.target.value)}
            placeholder="タナカ タロウ"
          />
        </div>
        <div>
          <button>SUBMIT</button>
        </div>
      </form>
    </div>
  )
}

export default InputForm;

ラベル(h2)とインプット(input)のセットが2つあり、コンポーネント化できそうです。

名前とカナ、それぞれの値は state で管理しています。

つまり今回のテーマは、入力欄をコンポーネント化するときに、このstateを「コンポーネント自身に持たせるか、親コンポーネントに持たせるか」ということになります。

stateをコンポーネント自身に持たせてみる

まず、stateをコンポーネント自身に持たせるパターンで実装してみます。

以下のような形になるでしょう。

import {useState} from "react";

function TextInput({ name, label, placeholder }: { name: string, label: string; placeholder: string }) {
  const [value, setValue] = useState('')
  return (
    <div>
      <h2>{label}</h2>
      <input
        name={name}
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder={placeholder}
      />
    </div>
  )
}

function InputForm() {
  return (
    <div>
      <form onSubmit={(e) => {
        e.preventDefault()
        console.log(new FormData(e.target).get('name'))
        console.log(new FormData(e.target).get('nameKana'))
      }}>
        <TextInput name={'name'} label={'名前'} placeholder={'田中 太郎'} />
        <TextInput name={'nameKana'} label={'名前(カナ)'} placeholder={'タナカ タロウ'} />
        <div>
          <button>SUBMIT</button>
        </div>
      </form>
    </div>
  )
}

export default InputForm;

TextInput というコンポーネントに抽出してみました。一見すると、親の InputForm からstateが消えてスッキリしたように見えます。

しかし、このコードではフォームの送信時に面倒なことになってしまいます。

子の TextInput にstateを持たせているので、親の InputForm からは入力欄にどんな値が入力されているのかがわかりません。

この方法だと、入力値を取得するタイミングがフォーム送信時しかないのです。

もっと言うと、フォームから値を取得するならTextInput にわざわざstateを持たせる必要がありません。

以下のように、useEffect で入力値に対してリアルタイムで処理をしたいということでもなければ、コンポーネント自身にstateを持たせる方法には特にメリットがなさそうです。

function TextInput({ name, label, placeholder }: { name: string, label: string; placeholder: string }) {
  const [value, setValue] = useState('')
  
  useEffect(() => {
    // 何かしらの処理
  }, [value])
  
  return (
    <div>
      <h2>{label}</h2>
      <input
        name={name}
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder={placeholder}
      />
    </div>
  )
}

stateを親コンポーネントに持たせてみる

もう一方のstateを親コンポーネントに持たせるパターンで実装してみます。

以下のような形になるでしょう。

import {ChangeEvent, useState} from "react";

function TextInput({ label, placeholder, onChange }: { label: string; placeholder: string, onChange: (event: ChangeEvent<HTMLInputElement>) => void; }) {
  return (
    <div>
      <h2>{label}</h2>
      <input
        type="text"
        onChange={onChange}
        placeholder={placeholder}
      />
    </div>
  )
}

function InputForm() {
  const [name, setName] = useState('');
  const [nameKana, setNameKana] = useState('');

  const handleClick = () => {
    console.log(name)
    console.log(nameKana)
  }

  return (
    <div>
      <form>
        <TextInput label={'名前'} placeholder={'田中 太郎'} onChange={(e) => setName(e.target.value)} />
        <TextInput label={'名前(カナ)'} placeholder={'タナカ タロウ'} onChange={(e) => setNameKana(e.target.value)} />
        <div>
          <button type={'button'} onClick={handleClick}>SUBMIT</button>
        </div>
      </form>
    </div>
  )
}

export default InputForm;

stateを親の InputForm に残し、JSXのみをコンポーネントにしてみました。

入力値はstateで管理されているのでフォームから取得する必要がなくなり、処理がスッキリしていることがわかると思います。

フォーム送信時でなくても自由に入力値にアクセスできるため、融通が効きそうな感じがしますね。

業務で管理画面などを実装していると、「入力エラーがある場合は送信ボタンを押せないようにする」という画面の仕様があったりします。

フォーム送信時にしか入力値が取得できない「stateをコンポーネント自身に持たせる」方法では、送信ボタンを押すまで「入力エラーがあるかどうか」がわからないので、この仕様を実現することはできません。
(やろうと思えばできなくもないですが、いびつな実装になりそうです)

一方、「stateを親コンポーネントに持たせる」方法では、任意のタイミングで入力値が取得できるので、この仕様は問題なく実現できるでしょう。

まとめ

いかがだったでしょうか?

今回は「入力欄のコンポーネントのstateは自身で持つか、親で持つか」ということについて考えてみました。

多くの場合、コンポーネント自身でstateを持つことにメリットはないので親で持つ方がよい、というのがこの記事での結論です。

ちなみに、コンポーネント自身にstateを持つものを「非制御コンポーネント(Uncontrolled)」、親コンポーネントでstateを持つものを「制御されたコンポーネント(Controlled)」といいます。

以下の React 公式の記事にて詳しく解説されています。

あわせて読みたい
コンポーネント間で state を共有する – React The library for web and native user interfaces

この手法に関しては、この記事の他にもたくさんの議論がされています。

ぜひ「react controlled uncontrolled」などでググって調べてみてくださいね。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

「できる」を増やしてワクワクな人生を送るための情報をシェアしています。

コメント

コメントする

目次