前提
・React初心者です。
・色々調べながらやっています。
useStateとは?
useStateとは、状態管理を行うためのReactHook。
状態管理という言葉が良くないと思う。何がしたいのかよくわからない。
ざっくり私の理解を記載すると、
useStateで宣言している変数の値が更新された場合に、再レンダリングを起こし、Domの更新を行うという機能。
基本パターン
const TestPage: React.FC = () => {
const [const, setCount] = useState(0);
// 何らかのデータを取得する処理
const data1 = getData();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>count Increment</button> // ボタンを押下する
</div>
)
}
ボタンを押下した際の動作は以下のようになる。
1. ボタンが押下されることによって、useStateの値が更新される。
2. 更新されることによって、TestPageコンポーネントが再レンダリングされる(再実行される)。
3. TestPage内の処理が再実行される。
詳細は↓
const TestPage: React.FC = () => {
const [count, setCount] = useState(0); // 再レンダリングに伴って、再実行するが、useStateだけは、初期化は行わず、setCount(count + 1)をした後の値が適用される。
const data1 = getData(); // 再レンダリングに伴って、再実行
return (
<div>
<p>Count: {count}</p> // 再実行に伴って再実行
<button onClick={() => setCount(count + 1)}>count Increment</button> // ボタンを押下する
</div>
)
};
useStateで宣言された変数が更新された場合、そのコンポーネント全体が再読み込みされるという動作になる。
これをReactでは再レンダリングと呼んでいる。
親子関係が絡むuseStateの利用パターン
一つ上のパターンを改造して、親子関係で、useStateを利用するパターン
const TestPage: React.FC = () => {
const [count, setCount] = useState(0);
const data1 = getData();
// 子コンポーネントに渡すclickイベント
const handleClick = () => {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
// 子コンポーネントを呼び出し
<Child onClick={handleClick}>
</div>
)
};
const Child: React.FC<{onClick: (count: number) => void}> = ({onClick}) => {
console.log("Childコンポーネント 再レンダリング")
return (
<div>
// 親コンポーネントから渡されたclickイベント
<button onclick={onClick}>
</div>
)
}
Childコンポーネントのボタンクリックした場合の動作は以下
1. Childコンポーネント内でボタンクリックする。→ useStateの値が更新される。
2. useStateを宣言しているTestPageが再レンダリングされる。
3. 親が再レンダリングされたことによって、Childコンポーネントも再レンダリングされる。
上記の様な動作となる。
useStateの動作として、「useStateの値が更新された場合、useStateを宣言したコンポーネントが再レンダリングされる」というのがポイントとなる。
まとめると、以下
1. ボタン押下
2. TestPageコンポーネントが再レンダリングされる。
3. TestPageコンポーネントが再レンダリングされたことを受けて、Childコンポーネントが再レンダリングされる。
useStateのライフサイクルについて
1. set関数の呼び出し → 更新をスケジュール(即時実行ではない)
2. 現在の関数を終了 → 変数は古いまま
3. 値が更新されたかどうかの確認
4. 再レンダリング開始 → 新しい値をセット
5. useEffect実行 → 副作用処理
上記の様なライフサイクルとなっている。
これらは今のところは理解できなくてもいい。
↓で検証してみたので、検証結果を持って、イメージが持てればいいと思う。
useStateについて詳しく調べてみた。
1. 非同期更新: set関数は非同期で実行される。
2. set関数はまとめられる: 複数のset関数は自動的にまとめられる
3. 関数型更新: setState(prev => prev + 1)の動作
4. Object.isで比較: 前回と今回が更新されているかどうかの判定は、Object.isで判定されている。
5. 初期化は一度だけ: 関数の場合も初回のみ実行
1. 非同期更新: set関数は非同期で実行される。
const TestPage: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`useEffect実行 - count:${count}(再レンダリング完了後)`)
}, [count]);
const handleSyncUpdate = () => {
console.log(`ボタンクリック開始 - 現在の値: ${count}`);
setCount(count + 1);
console.log(`1回目のsetCount(${count + 1})呼び出し直後 - count値: ${count}`);
setCount(count + 2);
console.log(`2回目のsetCount(${count + 2})呼び出し直後 - count値: ${count}`);
setTimeout(() => {
console.log(`setTimeout(10ms)で確認 - この時点でのcount値: ${count}`)
}, 10);
console.log(`handleSyncUpdate関数終了 - count値: ${count}`);
}
return (
<div>
<button onClick={handleSyncUpdate}>同期型更新</button>
</div>
)
}
こちらのボタンを押下した場合、どのような動作を行うかの検証をしてみる。
ちなみにuseEffectに関してはここでは詳しく説明しない。
ここでのuseEffectの動作の概要としては、useStateで宣言されているcount変数が変更されていた場合はuseEffect内の処理を実行する。という理解をしておけばOK。
いつか時間があるときにuseEffectの検証も詳しくやりたい。
それはそうと、以下のようなログが出た。
ボタンクリック開始 - 現在のcount: 0
1回目のsetCount(1)呼び出し直後 - count値: 0
2回目のsetCount(2)呼び出し直後 - count値: 0
handleSyncUpdate関数終了 - count値: 0
useEffect実行 - count値: 2(再レンダリング完了後)
setTime(10ms)で確認 - この時点でのcount: 0
一番注目してほしいのは、setTimeoutのログの部分。
★1 useEffectが先に実行されており、countが更新されている。
★2 setTimeoutの方が後に実行されているが、countの値が更新前の値になっている。
これは、ReactのuseStateで宣言された値は、再レンダリング前と再レンダリング後の変数が別の変数である。という動作を示している。
setTimeoutは非同期で動作するため、その時点の処理を切り出す。
つまり、再レンダリング前のcountを参照しているということになる。
一方で、再レンダリング後に動作したuseEffectは、値が書き換わっている。これは、再レンダリングによって、useState処理を再実行した際に、countに対して新しい値がセットされたためだと思われる。
勘違いが起こりやすいポイントとして、useStateは非同期で動作しているため、setTimeoutを使えば、いつかは値が変わるだろうという誤解があると思う。
だが、再レンダリング前の値をいくら待っても値が書き換わることはない。
大事なポイント!!!
useStateは、再レンダリングされた際に値が更新される!!
2. set関数はまとめられる: 複数のset関数は自動的にまとめられる
これについては、先ほどのソースを見て、おかしいと思ったと思う。もう一度先ほどのソースを見てみる。
const TestPage: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`useEffect実行 - count:${count}(再レンダリング完了後)`)
}, [count]);
const handleSyncUpdate = () => {
console.log(`ボタンクリック開始 - 現在の値: ${count}`);
setCount(count + 1);
console.log(`1回目のsetCount(${count + 1})呼び出し直後 - count値: ${count}`);
setCount(count + 2);
console.log(`2回目のsetCount(${count + 2})呼び出し直後 - count値: ${count}`);
setTimeout(() => {
console.log(`setTimeout(10ms)で確認 - この時点でのcount値: ${count}`)
}, 10);
console.log(`handleSyncUpdate関数終了 - count値: ${count}`);
}
return (
<div>
<button onClick={handleSyncUpdate}>同期型更新</button>
</div>
)
}
setCount(count + 1);
setCount(count + 2);
と合計3を足しているはずなのに、出てきたログは
useEffect実行 - count値: 2(再レンダリング完了後)
となっている。
このような記載をした場合は、一番最後の処理が実行される。
もっと例を出す
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // count = 0 再レンダリングされるまでは更新されない
setCount(count + 1); // count = 0 再レンダリングされるまでは更新されない
setCount(count + 1); // count = 0 再レンダリングされるまでは更新されない
// 結果: 3回実行しても 1 しか増えない
}
これは、countは再レンダリングされるまで、0なので、毎回(0 + 1)を実行しているため、1になるのでは?
と思ったのだが、どうやらそうでもないらしい。
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // count = 0 再レンダリングされるまでは更新されない
setCount(count + 2); // count = 0 再レンダリングされるまでは更新されない
setCount(count + 1); // count = 0 再レンダリングされるまでは更新されない
// 結果: 最後の値が適用され、count = 1となる。
}
こんなソースを試してみたところ、count=1となったため、1番最後の更新値が実行されるという動作となるようだ。
関数型更新(1つ↓で解説している)ではない更新方法では、set関数はまとめられて、一番最後の実行結果が反映される
3. 関数型更新: setState(prev => prev + 1)の動作
一方で、set関数で毎回カウントアップさせる方法がある。
これが、関数型更新とよばれている方法で、非常に簡単。
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1); // prev = 1
setCount(prev => prev + 1); // prev = 2
setCount(prev => prev + 1); // prev = 3
// 結果: prev = 3 となり、再レンダリング後のcountの値は 3 となる
}
上記のように記載すると、毎回カウントアップさせることができる。
注意点としては、実体のcountは、再レンダリングされるまで更新されないという点は変わらないこと!
4. Object.isで比較: 前回と今回が更新されているかどうかの判定は、Object.isで判定されている。
useStateは更新された場合は、コンポーネントの再レンダリングを実施する。
更新されていない場合は、コンポーネントの再レンダリングは起きない。
では、何が再レンダリングを引き起こして、何が再レンダリングを引き起こさないのか?という検証。
タイトルにも記載があるが、変更されているかどうかの判定は、Object.isで行われている。
これに関しては後でソースを記載するが、ここで重要なのは、Reactは内部的に、変更前の値を持っているということ。
変更前の値というのは、項番1のsetTimeoutの値であり、変更後というのが、useEffect処理を指す。
この変更前の値と変更後の値で比較を行い、変更されているかどうかの判定を行っている。(再レンダリングされているかの判定)
それの内部処理がObject.isですよ。という話。
const testObj = {
id: 1,
name: "name2"
}
const TestPage: React.FC = () => {
const [count, setCount] = useState(0);
const [obj, setObj] = useState(testObj);
useEffect(() => {
console.log(`CountのuseEffectの実行 ${count}`)
}, [count])
useEffect(() => {
console.log(`ObjのuseEffectの実行 ${obj}`)
}, [obj])
const handleCountClick = () => {
setCount(0);
}
const handleSameObjClick = () => {
setObj(testObj);
}
const handleDiffObjClick = () => {
setObj({...testObj});
}
return (
<div>
<button onClick={hancleCountClick}>count0を再セット</button>
<button onClick={handleSameObjClick}>同一Objを再セット</button>
<button onClick={handleDiffObjClick}>同一データだが、参照先が異なるobjを再セット</button>
</div>
)
}
以下の様な動作となる。
<button onClick={hancleCountClick}>count0を再セット</button>
→ プリニティブ型の場合は、現在と同一の値がセットされた場合、変更されていないと判定される。
<button onClick={handleSameObjClick}>同一Objを再セット</button>
→ 同一のObjを代入すると、参照先が同一のため、変更されていないと判定される。
<button onClick={handleDiffObjClick}>同一データだが、参照先が異なるobjを再セット</button>
→ 同一の値だが参照先が異なるObjを代入した場合、変更されていると判定される。
まとめ
- プリニティブ型の変数: 値を参照して変更管理
- 参照型変数: メモリ番地を参照して変更管理
5. 初期化は一度だけ: 関数の場合も初回のみ実行
これに関しては、今までで結構出てきていたので、簡単に説明。
1. コンポーネントの初回マウント
→ useStateの初期化
2. useStateの更新
→ コンポーネントの再レンダリング
3. useStateのset関数で設定された値をuseStateの値に再設定
→ const [const, setCount] = useState(0);
これのuseState(0)の初期化は実行せずに、設定された値をcountに設定する
というように、初期化はmountされた際に実行され、それ以降は実行されない。
最後に
Reactは自分なりに調べながらやっているので、間違っていることがあれば教えてください。