【React】テキストフォームの入力値を使用したstate更新に関するTips

ReactでSPAを構築する際に必須となるstateの更新に関するTipsをまとめます。(stateはVue.jsでいうdataプロパティみたいなものです)

React HooksのuseStateを使います。

言語はTypeScriptで書きます。

useStateを含む基本的なReact Hooksは別の記事でまとめています。

目次

state更新の基本

まずは通常の(よく教材で紹介されている)stateの更新の仕方はこんな感じ。
(超簡易的なログインページを例にします)

import { useState } from 'react'

export default function App () {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleChangeEmail = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value)
  }

  const handleChangePassword = (e: ChangeEvent<HTMLInputElement>) => {
    setPassword(e.target.value)
  }

  const handleClick = () => {
    // ログインAPIにPOSTする処理
  }

  return (
    <>
      <div>
        <span>メールアドレス</span>
        <input onChange={handleChangeEmail} value={email} />
      </div>
      <div>
        <span>パスワード</span>
        <input onChange={handleChangePassword} value={password} />
      </div>
      <div>
        <button onClick={handleClick}>送信</button>
      </div>
    </>
  )
}

「メールアドレス、パスワード用のinputタグのonChangeイベントにそれぞれhandleChangeEmailhandleChangePasswordという自分で定義したコールバック関数を設定して、その中でstateのset関数を読んでstateを更新する」というよくあるパターンです。

これは問題なく動作するのですが、このようにSPAでユーザーの入力値等のデータであるstateを管理するのはログインページだけでなく、もっと入力項目が多いページはあることは容易に考えられます。

が、今の実装方法だと入力フォームが増えるごとにhandleChange○○を追加する必要があります。

全てset関数を呼ぶだけという同じ処理内容なのに…です。

もっといい方法があります。

stateのオブジェクト型にして、stateの更新処理を共通化(1つに)する

では、以下の2つを反映してコードを拡張しやすいようにします。

  • stateをオブジェクト型にする
  • stateの更新処理を共通化する
import { useState } from 'react'

type FormData = {
  email: string
  password: string
}

export default function App () {
  // オブジェクト型のstateを定義
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: ''
  })

  // 共通化したstate更新処理
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target
    setFormData({ ...formData, [name]: value })
  }

  const handleClick = () => {
    // ログインAPIにPOSTする処理
  }

  return (
    <>
      <div>
        <span>メールアドレス</span>
        <input name='email' onChange={handleChange} value={formData.email} />
      </div>
      <div>
        <span>パスワード</span>
        <input name='password' onChange={handleChange} value={formData.password} />
      </div>
      <div>
        <button onClick={handleClick}>送信</button>
      </div>
    </>
  )
}

軽くコードの説明します。

まず、stateをオブジェクトで定義するところですね。

const [formData, setFormData] = useState<FormData>({
  email: '',
  password: ''
})

これで、stateを1つにまとめることができます。

またuseStateで定義するstateに型定義する時はジェネリクスを使ってuseState<型>とします。

で、肝になる共通化したstate更新処理です。

 const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
   const { name, value } = e.target
   setFormData({ ...formData, [name]: value })
 }

以下の手順でstateを更新します。

  • 分割代入でチェンジイベントのtargetからinputタグのname属性とvalue属性の値をそれぞれ変数に格納する
  • スプレッド構文で現在のstateと変更のあったプロパティのvalueを更新(上書きする)

この書き方は初めて見ると難しそうに見えますが慣れるととても便利です。(個人的には共通化できるメリットがかなりデカい)

このようにすることで、メールアドレス、パスワードの他にログインに必要な入力項目が増えたとしても、テキストフォームであればstateの更新処理は毎回増やす必要がなくなります。

個人的には現状はこれがベストプラクティスだと思っています!

onBlurイベントを使う(※非推奨)

※ここで紹介するやり方は非推奨です!
(詳しくはこのトピックの最後にまとめています。)

ここまで紹介したやり方はstateの更新をかけるタイミングはテキストフォーム(inputタグ)の内容が変わった時でonChangeイベントでhandleChangeというコールバック関数を実行しています。

1文字打つたびにstateが更新される、ということは1文字打つたびにコンポーネントが再レンダリングされるのでアプリケーション、コンポーネントの規模にもよりますが、パフォーマンス面でベストか?と言われたら何とも言えません。

レンダリングの回数を抑えるためにはonChangeイベントではなくonBlurイベントに変更することが効果的です。

onBlurイベントは「要素からフォーカスが外れた時に発火するイベント」です。テキストフォームに使うことで1文字打つごとにイベントが発火することがなくなりレンダリング回数が減ります。

JavaScript的には正しくはonclick、onblurと全て小文字ですが、Reactだとonの次は大文字で書く必要があります。

import { useState } from 'react'

type FormData = {
  email: string
  password: string
}

export default function App () {
  // オブジェクト型のstateを定義
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: ''
  })

  // 共通化したstate更新処理
  const handleBlur = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target
    setFormData({ ...formData, [name]: value })
  }

  const handleClick = () => {
    // ログインAPIにPOSTする処理
  }

  return (
    <>
      <div>
        <span>メールアドレス</span>
        <input name='email' onBlur={handleBlur} value={formData.email} />
      </div>
      <div>
        <span>パスワード</span>
        <input name='password' onBlur={handleBlur} value={formData.password} />
      </div>
      <div>
        <button onClick={handleClick}>送信</button>
      </div>
    </>
  )
}

こうすることでレンダリング回数を減らすことができます。

と、ここで終わらせたいですが実は上のコードではコンソールに以下のエラーが出力されます。

Warning: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.

valueを設定する時はonChangeイベントを設定する必要があります。

Reactによってvalueが変更されるコンポーネントのことを「制御コンポーネント」、変更されないコンポーネントを「非制御コンポーネント」と言い、onBlurでstateを更新する場合は非制御コンポーネントとなります。

React公式ドキュメントに以下のように記載されています。

「ほとんどの場合では、フォームの実装には制御されたコンポーネント (controlled component) を使用することをお勧めしています。」


Reactでは基本的には制御コンポーネントの使用を推奨しているため、”どうしても制御コンポーネントだと機能が実装できない場合”を除き、非制御コンポーネントは使わない方が良いと思いました。
(知り合いのReactエンジニアにヒアリングした結果なのでこれが絶対解とか限りませんが、React公式が非推奨としているのは事実です)


ちなみに制御コンポーネントは公式ドキュメントで以下のように説明されています。

「React の state を “信頼できる唯一の情報源 (single source of truth)” とすることで、上述の 2 つの状態を結合させることができます。そうすることで、フォームをレンダーしている React コンポーネントが、後続するユーザ入力でフォームで起きることも制御できるようになります。このような方法で React によって値が制御される入力フォーム要素は「制御されたコンポーネント」と呼ばれます。」

ざっくり言うと、value属性をstateの値で制御しているコンポーネントですね。

制御コンポーネントについて↓

非制御コンポーネントについて↓

では非推奨ではありますがonBlurを使ったstate管理についてまとめます。

onBlurイベントを使う時の実装パターン(※非推奨)

※繰り返しになりますが、ここで紹介するやり方は非推奨です!

defaultValueと組み合わせる(※非推奨)

import { useState } from 'react'

// 略

export default function App () {
  // 略

  return (
    <>
      <div>
        <span>メールアドレス</span>
        <input name='email' onBlur={handleBlur} defaultValue={formData.email} />
      </div>
      <div>
        <span>パスワード</span>
        <input name='password' onBlur={handleBlur} defaultValue={formData.password} />
      </div>
      <div>
        <button onClick={handleClick}>送信</button>
      </div>
    </>
  )
}

上記のようにdefaultValue属性を設定することでエラーが出力されなくなります。

そもそもですがdefaultValuevalueも)を使うケースはこの画面で何かしらを入力した後、他の画面(コンポーネント)に遷移→その画面から戻った時に入力フォームへの入力値を保持する必要がある場合だと思いますが、コンテキストに保存したユーザー入力値から該当の値をdefaultValueで持っておけば前の画面に戻った時に入力フォームに値を保持できます。

非制御コンポーネント(=非推奨)になりますが、実装上は問題なさそう。

↑実装上は問題ないですが、基本的には制御コンポーネント(onChange + value)を使うのが推奨されてます!!

valueを持たない(※非推奨)

import { useState } from 'react'

// 略

export default function App () {
  // 略

  return (
    <>
      <div>
        <span>メールアドレス</span>
        <input name='email' onBlur={handleBlur} />
      </div>
      <div>
        <span>パスワード</span>
        <input name='password' onBlur={handleBlur} />
      </div>
      <div>
        <button onClick={handleClick}>送信</button>
      </div>
    </>
  )
}

個人的にはこれも全然アリかなと思っています。

↑実装上は問題ないですが、基本的には制御コンポーネント(onChange + value)を使うのが推奨!!

再レンダリングの回数を抑えるための対策として非推奨なのは承知の上でonBlurを使った時の実装パターンを2つ挙げました。

最後に

結論、

onChangeとvalue属性を使って制御コンポーネントを使おうね!

というお話でした。

仕事でReactを使っているエンジニア数名に意見をもらったところ

  • 基本的にライブラリやフレームワークが推奨するやり方に従うのが色々な面で無難
  • レンダリングコスだけで考えると「onBlur + defaultValue」は効果的かもしれないけど、「onChange + value」でのレンダリングコストがUXに影響するレベルであることがわかってからレンダリング最適化を対応すれば良いと思っている
  • 基本的にはReact-Hook-Formというライブラリを使っている

このような意見をもらったので敢えて非推奨(アブノーマル)なやり方を採用するメリットもなさそうだなと思いました。

(そもそも今業務で開発しているSPAは本番での動作検証はする前なのでこの段階で「レンダリング回数が〜」とかは気にする必要がなかったですね)

SPAはデータをリアクティブに管理するだけでも色々考えないといけないことがあるので大変だなと痛感しています…

引き続き開発の中で得た知見は言語化してまとめておこうと思います!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!

この記事を書いた人

Webエンジニア←KHI(プラント事業の技術職)←大学院(機械工学)

PHP/Laravel/TypeScript/React/Next.js(Vue.js/Nuxt.jsは少し)
バックエンド歴の方が長いです。

神戸で「つながる勉強会」を運営↓
https://tsunagaru-kobe.connpass.com/

コメント

コメントする

目次
閉じる