※公式リファレンスをもとに自分なりにまとめたものになります。
useStateはコンポーネント固有のメモリであるstate変数を追加するためのHook。
普通にローカル変数に値を入れればいいんじゃ?と思うかもしれませんが、ローカル変数はレンダー間(UI描画)で保持されないし、ローカル変数が更新されてもレンダーはトリガされません。
つまり、ローカル変数が変更されたことはレンダーでは考慮されることがないのです。
useState変数を追加することで、
- レンダー間で値が保持される
- 変数を更新するとReactがコンポーネントを再レンダーするようにトリガする
この二つを実現できます。
追加方法
ファイルの先頭でインポートします
import { useState } from 'react';
state変数を以下のように追加します
const [index, setIndex] = useState(0);
↑の構成は以下のようになっています
index
...state変数
setIndex
...セッタ関数(state変数を更新し、 再レンダーをトリガするための関数)
(0)...state変数の初期値0
たとえば、このようにハンドラの処理に書くことで値を更新することができます
function handleClick(){
setIndex(index + 1);
}
また、stateの値が前のstateの値に依存する場合(カウンタを増やしたり、配列に要素を加えたりする場合)、
setIndex(prev => prev + 1);
このような関数型更新が推奨されます。
関数型更新を使うと、Reactが保持している最新のstateをprevとして受け取り、その上で次の値を計算できます。
これは、連続更新の際に古い値(古いクロージャ)が参照されることを防ぐために有効です。
このuseStateを含めて、useから始まるHookはReactがレンダーされている間のみ使用可能です。
また、呼び出せるのは関数コンポーネントのトップレベルかカスタムフックのトップレベルのみです(条件分岐の中、ループやネストされた関数の中では呼び出せません)
複数のstateをまとめる
たとえば名前(firstName, middleName, lastName)や、位置セット(x, y, width, height)など、複数のstate変数を同時に変更することがある場合はグループ化して、単一のstate変数にまとめることができます。
const [name, setName] = useState({
firstName: "Ethan",
middleName: "James",
lastName: "Carter",
});
const [position, setPosition] = useState({
x: 0,
y: 0
});
このようにstate変数にオブジェクトを渡した場合、基本的にどれか一つだけ更新することはできません(useStateはマージではなく置き換えであるため)。
setPosition({ x: 10 }); // y が消える(undefined になる)
ただし、スプレッドして置き換える方法であれば部分更新が可能になります。
setPosition(prev => ({ ...prev, x: 10 })); // x だけ更新
setPosition(prev => ({ ...prev, y: prev.y + 5 })); // y だけ更新
また、既存のpropsの値やstate変数から算出できる情報は、冗長になるためコンポーネントのstateに入れるべきではありません。
useStateの引数に関数を渡す
たとえば、
function createInitialTodos() {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: 'Item ' + (i + 1)
});
}
return initialTodos;
}
このような関数があり、これを
const [todos, setTodos] = useState(createInitialTodos());
このように初期stateとしてuseStateの初期値に渡すとします。
その場合、目に見える挙動は意図したものになるはずですが、この関数はテキストフィールドに文字を入力したときなど、すべてのレンダー時に再実行されることになり、効率が悪くなります。
しかし、以下のように、
const [todos, setTodos] = useState(createInitialTodos);
関数の戻り値ではなく、関数そのものをuseStateの初期値に渡すことで、それを回避できます。
これは初期化関数と言って、コンポーネントの再レンダー時ではなく、初期化時にのみ実行されます。