問題
問題です。以下のコンポーネントでボタンをクリックすると最終的に useItems
から返ってくる items
はどのような値になるでしょうか?
import React, { useEffect, useState, useRef } from "react";
const useItems = () => {
const [items, setItems] = useState([])
const idRef = useRef(0)
const push = () => {
const id = idRef.current++
setItems(items.concat({ id }))
}
return {
items,
push,
}
}
export default function App() {
const { items, push } = useItems()
return (
<div className="App">
<button
type="button"
onClick={() => { push(); push(); push() }}
>
PUSH PUSH PUSH!!!
</button>
<ul>
{items.map((item) => <li key={item.id}>{item.id}</li>)}
</ul>
</div>
)
}
…
…
メタ読みすれば [0, 1, 2]
ではないですよね…
…
…
正解は [2]
です!
解説
何故でしょう?
push
はクロージャから items
をとってきて setItems(items.concat({ id }))
をしていますが items
自身が更新されるわけではありません。
なので3回呼ばれている同一の push
では毎回 setItems([].concat({ id }))
され、最終的には setItems([2])
されるからです。
setState
は引数として (currentState) => nextState
な関数を受け取ることができます(Functional updates)。
これを使って上のような問題が起きないように書き換えましょう。
import React, { useEffect, useState, useRef } from "react";
const useItems = () => {
const [items, setItems] = useState([])
const idRef = useRef(0)
const push = () => {
const id = idRef.current++
setItems((currentItems) => currentItems.concat({ id }))
}
return {
items,
push,
}
}
export default function App() {
const { items, push } = useItems()
return (
<div className="App">
<button
type="button"
onClick={() => { push(); push(); push() }}
>
PUSH PUSH PUSH!!!
</button>
<ul>
{items.map((item) => <li key={item.id}>{item.id}</li>)}
</ul>
</div>
)
}
こうすれば useItems
から返ってくる items
は期待通り [0, 1, 2]
になります。
なんとなく書いていると意外とあれ?と思うことが起きがちかもなと思いました。
実際に触れるCodeSandboxを置いておきます↓
https://codesandbox.io/s/blissful-robinson-329w2?file=/src/App.js