そもそもフック(hook)とは?
フックはReact16.8で追加された機能であり、
これによりstateなどのReactの機能を、クラスを書かずに使えるようになりました。
useStateとは?
useStateは関数コンポーネントにてstateを保持・更新するための機能です。
初回のレンダリングで、初期値となる値をuseStateに引数として渡し、その値が1つ目の要素のstateとなります。
2つ目の要素はstate値を更新するための関数となり、基本的にこの関数を使ってstate値を更新します。
以下のように定義します。
import React, { useState } from 'react';
const Example = () => {
const [state, stateを更新する関数] = useState(初期値);
~~~~
useStateを試してみる
import React, { useState } from 'react';
export const Example = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>ボタンが {count} 回押されました</p>
<button onClick={() => setCount(count + 1)}>
ボタン
</button>
</div>
)
};
countというstateを宣言しています。
ボタンが押されると、stateの更新する関数であるsetCountを呼び出し、
countの値に1をプラスします。
これによりボタンが押されるたびにcountが増えていきます。
stateを更新する関数はstate名の頭にsetをつけるという命名が一般的ですが、
必ずその命名にする必要はありません。
ちなみに、stateの更新する関数は関数を渡すことも可能です。
import React, { useState } from 'react';
export const Example = () => {
const [count, setCount] = useState(0);
const handleOnClick = () => {
setCount(updateCount);
}
return (
<div>
<p>countの値は{count} です</p>
<button onClick={handleOnClick}>
ボタン
</button>
</div>
)
};
const updateCount = (count: number) => count + 1
このように更新ロジックをコンポーネント外に切り出すことができるので
覚えておいて損はないかと思います。
useStateで気を付けること
stateが作成されるのはコンポーネントの初回レンダリング時だけ
再レンダリングされても再度stateが作成されるわけではありません。
必ずトップレベルで呼び出す
ループや条件分岐・ネストされた関数内などで呼び出してはいけません。
これによって、コンポーネントがレンダリングされる際のフックが呼ばれる順番を保証します。
関数コンポーネント、もしくはカスタムフック内で呼び出す
ちなみにカスタムフックとは、React hooksの処理をコンポーネントに直接書かずに、別ファイルに切り出して新しいhooksとして定義した関数のことです。
stateの値を更新するときは、必ず更新関数を使う
例えば以下のような処理があるとします。
import React, { useState } from 'react';
export const Example = () => {
const [count, setCount] = useState(["a", "b", "c"]);
const handleOnClick = () => {
count.push("d");
setCount(count);
console.log(count); // -> (4) ['a', 'b', 'c', 'd']
}
return (
<div>
<p>countの値は{count} です</p>
<button onClick={handleOnClick}>
ボタン
</button>
</div>
)
};
実行結果
コンソール
(4) ['a', 'b', 'c', 'd']
ボタンが押された場合、handleOnClickが実行されてcountに文字列の「d」を追加していますが、
ボタンをいくら押しても画面に表示されているcountの値は変わりません。
count.push("d");
setCount(count);
ここで更新の関数を使っているのになぜ変わらないのかというと、
stateの変更の判定にObject.is()が使用されているからです。
そのため同じ配列のstateを更新したところで、
変更が検知されずに再レンダリングも行われないということです。
ではどうすれば良いかというと、スプレッド構文で新たに配列を作成してあげて、
更新関数に渡してあげます。
配列の更新は、基本的にこれが推奨されているようです。
import React, { useState } from 'react';
export const Example = () => {
const [count, setCount] = useState(["a", "b", "c"]);
const handleOnClick = () => {
setCount([...count, "d"]);
}
return (
<div>
<p>countの値は{count} です</p>
<button onClick={handleOnClick}>
ボタン
</button>
</div>
)
};
実行結果
これらのルールが守らないと、stateの保持が正しく行われなくなります。
useStateで更新したstateはすぐに更新されない
先ほどの処理にconsole.logを仕込んでみます。
import React, { useState } from 'react';
export const Example = () => {
const [count, setCount] = useState(["a", "b", "c"]);
const handleOnClick = () => {
setCount([...count, "d"]);
console.log(count); // -> (3) ['a', 'b', 'c']
}
return (
<div>
<p>countの値は{count} です</p>
<button onClick={handleOnClick}>
ボタン
</button>
</div>
)
};
コンソール
(3) ['a', 'b', 'c']
これは更新関数の呼び出しが非同期なので、
次のレンダリングされるタイミングまでstateが反映されないということです。
そんなときはuseEffectを使うと良いかと思います。
countが変更されたときにuseEffectが実行され、
この時にレンダリングもされるので、コンソールにもcount更新後の値を表示することができました。
import React, { useEffect, useState } from 'react';
export const Example = () => {
const [count, setCount] = useState(["a", "b", "c"]);
useEffect(() => {
console.log(count) // -> (4) ['a', 'b', 'c', 'd']
}, [count])
const handleOnClick = () => {
setCount([...count, "d"]);
}
return (
<div>
<p>countの値は{count} です</p>
<button onClick={handleOnClick}>
ボタン
</button>
</div>
)
};
まとめ
更新関数の挙動や、stateが即時反映されないなど、誰しもが詰まったことがあるのではないでしょうか?
useStateは簡単に見えて実はかなり奥が深いですね。
参考