🐷

配列を用いたuseStateの落とし穴

2022/08/12に公開4

useStateで状態管理ができる。

import { useState } from "react"
export default function Home() {
	const [val, setVal] = useState("hello")
	function handle() {
		setVal("world")
	}
	return <div onClick={ handle }>{ val }</div> // hello
}

という感じでsetValを使ってvalに変更を加える。

上の例ではvalは文字列だが配列を扱いたい時は

import { useState } from "react"
export default function Home() {
	const [arr, setArr] = useState([1, 2, 3])
	function handle() {
		let tmp = arr.map(a => a)
		tmp[2] = 100
		setVal(tmp)
	}
	return (
	<>
		arr.map(a => <div id={ a }>{ a }</div>)
		<div onClick={ handle }></div>
	</>
	)
	return <div onClick={ handle }>{ va }</div> // [1, 2, 3]
}

ここでhandle関数では直接変更せずにtmpに入れていることに注意。

ここがオブジェクトの配列だと以下のような罠にハマる

import { useState } from "react"
export default function Home() {
	const [arr, setArr] = useState([{name: "Sato", age: 23}, {name: "Yogo", age: 12})
	function handle() {
		arr[1].name = "Taro"
		console.log(arr) // 値は変更されている
		setArr(arr) // 値は変更されない
	}
	return (
	<>
		arr.map(a => <div id={ a }>{ a }</div>)
		<div onClick={ handle }></div>
	</>
	)
	return <div onClick={ handle }>{ va }</div> // [1, 2, 3]
}

constは変更できないが要素は取り出して変更できてしまう。見かけ上は変更したarrをsetArrしているので混乱するが次のようにする。

import { useState } from "react"
export default function Home() {
	const [arr, setArr] = useState([{name: "Sato", age: 23}, {name: "Yogo", age: 12})
	function handle() {
		arr[1].name = "Taro"
		let tmp = arr.map(a => a);
		tmp[1].name = "Taro"
		console.log(tmp) // 値は変更されている
		setArr(tmp) // 値は変更される
	}
	return (
	<>
		arr.map(a => <div id={ a }>{ a }</div>)
		<div onClick={ handle }></div>
	</>
	)
	return <div onClick={ handle }>{ va }</div> // [1, 2, 3]
}

わからないこと

  1. constは要素を取り出して変更できてしまっていいのか(そもそもこれ変更できてる?)
  2. useStateで指定された変数はどう扱われているのか
  3. こうしたケースのベストプラクティス(おそらくスプレッド構文を使う?)

まだ理解が足りない。

Discussion

XU ZHONGWEIXU ZHONGWEI

クリックによるアップデート情報をすでにfiberNodeのmemorizedStateのqueueに打ち込んあります。
しかし、Object.Is(前のarr,今のarr)の値はtrueだから、再レンダリングを促していないため、新しいarrも計算できてなく、domにも反映されてないとのことです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is

スプレッド構文により、arrはまっさら新しいインスタンスとして作成され、Object.Is(前のarr,今のarr)の値はfalseだから、再レンダリングが走ります。当然、arrも計算され、domにも反映されます。

もしTaroを変更しようとした後、別のstateの変化によりでレンダリングが走ったら、Taroもついでに計算でき、domにも反映されます。
例えば

import { useState } from "react";

function App() {
  const [arr, setArr] = useState([{name: "Sato", age: 23}, {name: "Yogo", age: 12}])
	
  const [num, setNum] = useState(0)
	function handle() {
    arr[1].name = "Taro"
		console.log(arr) // 値は変更されている
		setArr(arr) // 値は変更されない
	}
	return (
	<>
	  {arr.map(a => <div key={a.name}>{ a.name }</div>)}	
		<div onClick={ handle }>click</div>
    <div><button onClick={_=>setNum((num)=>num + 1)}>click here and all will change</button></div>
	</>
	)
}

export default App;
  1. clickのところをクリックし、何も反映しません
  2. "click here and all will change"をクリックして、Taroが反映されます

試しているReactバージョンは17です。

nekoneko

ありがとうございます。少しわからないのですが

arr[1].name = "Taro"

の時点で(consoleでも明らかな通り)確かにarrの内容は変更されているのにObject.is(前のarr,今のarr)がfalseになるのは何故なのでしょうか?

XU ZHONGWEIXU ZHONGWEI

arr[1].name = "Taro"だと、arrは変更されてますが、arrは再作成していないです。メモリのアドレスも変わっていないです。前のarrと今のarr実は同じメモリアドレスを指しています。

nekoneko

メモリアドレスを見てるのですね。ありがとうございます理解が進みました。