ReactでSPAを構築する際に必須となるstateの更新に関するTipsをまとめます。(stateはVue.jsでいうdataプロパティみたいなものです)
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
イベントにそれぞれhandleChangeEmail
、handleChangePassword
という自分で定義したコールバック関数を設定して、その中でstateのset関数を読んでstateを更新する」というよくあるパターンです。
これは問題なく動作するのですが、このようにSPAでユーザーの入力値等のデータであるstateを管理するのはログインページだけでなく、もっと入力項目が多いページはあることは容易に考えられます。
が、今の実装方法だと入力フォームが増えるごとにhandleChange○○
を追加する必要があります。
もっといい方法があります。
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
イベントに変更することが効果的です。
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>
</>
)
}
こうすることでレンダリング回数を減らすことができます。
と、ここで終わらせたいですが実は上のコードではコンソールに以下のエラーが出力されます。
valueを設定する時はonChangeイベントを設定する必要があります。
Reactによってvalueが変更されるコンポーネントのことを「制御コンポーネント」、変更されないコンポーネントを「非制御コンポーネント」と言い、onBlur
で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属性を設定することでエラーが出力されなくなります。
そもそもですがdefaultValue
(value
も)を使うケースはこの画面で何かしらを入力した後、他の画面(コンポーネント)に遷移→その画面から戻った時に入力フォームへの入力値を保持する必要がある場合だと思いますが、コンテキストに保存したユーザー入力値から該当の値をdefaultValue
で持っておけば前の画面に戻った時に入力フォームに値を保持できます。
非制御コンポーネント(=非推奨)になりますが、実装上は問題なさそう。
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>
</>
)
}
個人的にはこれも全然アリかなと思っています。
再レンダリングの回数を抑えるための対策として非推奨なのは承知の上でonBlur
を使った時の実装パターンを2つ挙げました。
最後に
結論、
というお話でした。
仕事でReactを使っているエンジニア数名に意見をもらったところ
- 基本的にライブラリやフレームワークが推奨するやり方に従うのが色々な面で無難
- レンダリングコスだけで考えると「onBlur + defaultValue」は効果的かもしれないけど、「onChange + value」でのレンダリングコストがUXに影響するレベルであることがわかってからレンダリング最適化を対応すれば良いと思っている
- 基本的には
React-Hook-Form
というライブラリを使っている
このような意見をもらったので敢えて非推奨(アブノーマル)なやり方を採用するメリットもなさそうだなと思いました。
(そもそも今業務で開発しているSPAは本番での動作検証はする前なのでこの段階で「レンダリング回数が〜」とかは気にする必要がなかったですね)
SPAはデータをリアクティブに管理するだけでも色々考えないといけないことがあるので大変だなと痛感しています…
引き続き開発の中で得た知見は言語化してまとめておこうと思います!
コメント