🚨

NGパターンで見る、useStateでオブジェクト型を扱う方法

2023/11/09に公開

useState()でオブジェクトを扱う方法を、自分がやってしまったNGパターンを添えながら書いていきたいと思います。

useState()が理解できてきたけど、まだそんなに使いこなせていないというような初学者の方向けに書いています。(僕自身がが初学者なので、それ以上のものは提供できないというのもある)

定義の方法自体はプリミティブ型の時と同じ

オブジェクト型だから定義の方法が変わるということはなく、useStateの初期値にオブジェクトを入れるだけです。

サンプルコードは、タイマーの開始時間と終了時間をタイムスタンプとして取得・更新を行うuseState()です。開始/終了をオブジェクトで表現しています。

sample.tsx
const [timeStanp, setTimeStanp] = useState({
  start: 0,
  end: 0
})

もちろん以下のように、オブジェクトの初期値を変数に切り出すこともできます。ついでに型定義も追加しておきました。(型定義の説明は割愛)

sample.tsx
type PropertyType<T extends string | number | symbol> = {
  [K in T]: number;
};

const timeStanpObj:PropertyType<'start'|'end'> = {
 start: 0,
 end: 0
}
const [timeStanp, setTimeStanp] = useState(timeStanpObj)

更新関数の書き方

オブジェクト型を持つステートの更新関数は、基本的には以下のような書き方をします。
以下の例は、タイマーを開始したときの時間(ミリ秒)と、終了時間(ミリ秒+タイマーの所要時間)をセットする更新関数です。

sample.tsx
setTimeStanp({start: Date.now(), end: Date.now() + initialCount})

//プリミティブ型の時だとこんな感じですよね。
//setTimeStanp((state) => Date.now())

プリミティブ型を扱う時とは異なり、更新関数の中身もオブジェクトで定義することになります。

オブジェクトの一部だけ更新したい時にはどうすればいい?

すべての値を同時に更新する場合には上述の方法でOK。しかし、オブジェクトの中の一部のみを更新したい場合にはどうすればよいでしょう?自分はここでハマってしまいました。

まずは自分がやってしまったNGな書き方を紹介します。
例えば、オブジェクトのstartの値だけを更新したいとします。

❌NGパターン:更新したい値だけを記述しちゃう

sample.tsx
// startの値だけを更新したいときのNGパターン。エラーになる。
setTimeStanp({start: Date.now()})

この書き方のなにがダメかというと、更新したい値だけを記述している点です。
useStateはあくまでもオブジェクト単位で状態を保持・更新しているため、更新をしたい値だけを取り出して処理を行うことはできません。

🔵OKパターン

OKパターンです。更新を行わない値も省くことはできないため、そのままにしておきたい値は以下のように現在の状態を渡す形で記述します。

sample.tsx
//timeStanp.startの値だけを更新したい時
setTimeStanp({start: Date.now(), end: timeStanp.end})

//timeStanp.endの値だけを更新したい時
setTimeStanp({start: timeStanp.start, end: Date.now() + initialCount})

オブジェクト型の状態は、上記のようにドット記法でオブジェクト内のキー名を呼んできじゅつすることができます。

また、オブジェクトの値がたくさんある場合は、Spread構文を用いてまとめて渡してあげる事もできます。(多分こっちを使ってる人のほうが多いんじゃなかろうか)

sample.tsx
//Spread構文を用いた例

//timeStanp.startの値だけを更新したい時
setTimeStanp({...timeStanp, start: Date.now()})

//timeStanp.endの値だけを更新したい時
setTimeStanp({...timeStanp, end: Date.now() + initialCount})

直前の状態(prevState)を使いたい時

直前の値を使って更新を行う、というのはuseStateあるあるだと思います(クリックすると値がインクリメントしていく、、、とか)

今回は例として、timeStanp.endの、直前の状態に対して処理を行いたいとします。(処理の内容は適当です)これも自分がやってしまったNGパターンを先にご紹介します。

❌NGパターン:関数をブチ込む

sample.tsx
//NGパターン。型ガン無視で草。
setTimeStanp({...timeStanp, end: (prev) => prev + 1})

//プリミティブ型のときだとこうやりますよね。
//setSampleState((prevState) => prevState + 1)

前知識無しで挑んでいたので、いつものイメージが先行してこうなってしまいました。そもそも論としてendはnumber型しか受け付けない型定義にしているので関数はセットできません。当然エラー。

🔵OKパターン

OKパターンです。以下のコードのように、オブジェクト全体を関数でラップするのが正しい書き方です。

sample.tsx
//オブジェクト全体を関数でラップしている。
setTimeStanp((prev) => ({...timeStanp, end: prev.end + 1 }))

引数prevは、timeStampの直前の状態を表現しているので、timeStanp.endの直前の状態はprev.endという風に置き換えることができます。
useStateあくまでもオブジェクト単位で状態を保持・管理しているという理解があれば、上記の書き方に自然とたどり着けたんだろうなと思いますw

まとめ

useStateでオブジェクト型の状態管理を行う場合は、あくまでもオブジェクト単位で状態を保持・管理しているという理解が重要!

Discussion