教材
ShinCodeさんの
【完全保存版】React Hooksを完全に理解するHooksマスター講座【React18~19対応】
を元に、私が学んだ内容をまとめていきますわ。
お嬢様的Hooks解説
ReactHooksとは何か?
Hook: 何かを引っ掛けるという意味ですわね。
React Hooksは、関数コンポーネントとreactの機能を結びつけるための仕組みでございますわ。Hooksを使用することで、関数コンポーネント内で状態管理や副作用の処理などを行うことができるのです。
React バージョン16.8以前では、主にクラスコンポーネントが使用されておりました。そのため、状態管理やライフサイクルメソッドの利用には、クラスを用いてコンポーネントを書く必要がございました。
かつての状況
以前は、Reactコンポーネントをクラスで書くことが一般的でした。クラスコンポーネントを使用することで、this.state
やthis.setState
を利用して、状態管理し、ライフサイクルメソッドを使って副作用の処理を行っていたのですわ。
現在の状況
しかし、React Hooksが導入された現在では、クラスコンポーネントを理解する必要がございませんの。Hooksを利用することで、よりシンプルで直感的なコードを書くことが可能になり、関数コンポーネント内で状態や副作用の管理を容易に行うことができるのです。
Hooksの重要性
特に、どのタイミングでAPIを発火させるかなどの制御が必要な場合に、Hooksは非常に重要ですわ。例えば、useEffect
フックを使用することで、副作用を明示的に管理し、コンポーネントのマウントやアンマウント時に特定に処理を行うことができます。
useState()を使わないstate管理
ボタンを押すたびにageが加算されていくコードを、useStateを使わずに記述すると以下のようになりますわ。
let age = 0;
const Lesson1_1 = () => {
const handleClick = () => {
age = age + 1; //数値が変わっているか確認する
}
return (
<div>
<input type="text" />
<button onClick={handleClick} className="border p-2 rounded-md bg-red-100">Add Age</button>
<p>You are {age}</p>
</div>
);
};
export default Lesson1_1;
問題点
しかしながら、ボタンを押しても画面のage
が変動しませんの。console.log(age)
で確認すると、数値は確かに変わっておりますのに、画面上ではその変化が反映されていないようですわ。
理由
なぜ画面が変わらないのかと申しますと、Reactの再レンダリングが発生していないためですの。Reactはstate
の変更を感知して再レンダリングを行いますが、単なる変数の変更では再レンダリングが発生しないのですわ。
useState()
useStateフックは、値をメモ化し、状態の変化に伴って再レンダリングを行うためのReactの機能ですの。
import { useState } from "react";
const Lesson1_1 = () => {
const [age, setAge] = useState<number>(0);
const handleClick = () => {
setAge(age + 1);
console.log(age);
}
return (
<div>
<input type="text" />
<button onClick={handleClick} className="border p-2 rounded-md bg-red-100">Add Age</button>
<p>You are {age}</p>
</div>
);
};
export default Lesson1_1;
このように、const [state, setState] = useState();
と記述して、状態管理を行いますの。
Reactのバッチ処理
Reactはバッチ処理を行うため、以下のコードではクリックするたびに3ずつ値が上がるわけではございませんの。
const handleClick = () => {
setAge(age + 1);
console.log(age);
setAge(age + 1);
console.log(age);
setAge(age + 1);
console.log(age);
};
この場合、クリックするたびに1ずつしか値が上がりませんわ。これは、Reactが状態の更新をバッチ処理し、最後のsetAge
で再レンダリングが行われるためですの。
正しい方法:3ずつ値を増やす
3ずつ値を増やしたい場合は、以下のようにsetAge
に関数を渡しますの
const handleClick = () => {
setAge((prevAge) => prevAge + 1); // setAge((0) => 0 + 1) これの返り値は1
setAge((prevAge) => prevAge + 1); // setAge((1) => 1 + 1) これの返り値は2
setAge((prevAge) => prevAge + 1); // setAge((2) => 2 + 1) これの返り値は3
}
この方法は、setAge
に関数を渡すことで、前の状態を元に新しい値を計算し、その値を返り値として設定するものですの。
詳細説明
-
setAge((prevAge) => prevAge + 1);
:prevAge
は現在のage
の値を指し、prevAge + 1
は更新後の値となりますの。 - これを3回繰り返すことで、
age
が3ずつ増加いたしますの。
レンダリングのタイミング
Reactにおけるレンダリングのタイミングは以下の通りでございますわ:
-
ページを更新した時(初回レンダリング):
コンポーネントが初めて描画される際に発生しますの。 -
状態変数が更新されたタイミング(useStateのset〇〇など):
状態が変化すると、対応するコンポーネントが再レンダリングされますの。
レンダリングのタイミングを理解することは、パフォーマンスチューニングに繋がりますの。例えば、useMemo
やuseCallback
を使用して、不要な再レンダリングを防ぐことができますの。
Strictモード
ReactのStrictモードでは、開発環境においてコンポーネントが2回呼び出されることがございますわ。これは、純関数の特性を確認するための措置ですの。
純関数とは
純関数は、以下の特性を持つ関数でございますわ。
- 同じ入力に対して必ず同じ出力を返しますの。
- 関数の外部に影響を及ぼす副作用を持ちませんの。
例えば、y = 2x
という関数があった場合、x
が2ならy
は4になり、x
が4ならy
は8になりますの。入力が同じであれば、出力も必ず同じであるべきですの。
【おすすめ】
Strictモードの目的
Strictモードは、意図しない挙動がないか確認するために、開発中にコンポーネントを2回呼び出しますの。これにより、純関数であるかどうかを検証し、不具合を早期に発見して修正することができますの。
onChangeトリガーを使ったinput値の状態更新
以下のコードは、onChange
トリガーを使用して、input
の値を状態として管理する例でございますわ。
import { ChangeEvent, useState } from "react";
const Lesson1_1 = () => {
const [age, setAge] = useState<number>(0);
const [name, setName] = useState<string>("ShinCode");
console.log("レンダリング");
const handleClick = () => {
setAge((prevAge) => prevAge + 1); // setAge((0) => 0 + 1) これの返り値は1
setAge((prevAge) => prevAge + 1); // setAge((1) => 1 + 1) これの返り値は2
setAge((prevAge) => prevAge + 1); // setAge((2) => 2 + 1) これの返り値は3
console.log(name);
}
return (
<div>
<input type="text" value={name} onChange={(e: ChangeEvent<HTMLInputElement>) => setName(e.target.value)} />
<button onClick={handleClick} className="border p-2 rounded-md bg-red-100">Add Age</button>
<p>You are {age}</p>
</div>
);
};
export default Lesson1_1;
このコードでは、input
要素にonChange
トリガーを設定し、setName
を使用してname
の状態を更新しておりますの。これにより、入力した値がname
に保持されますの。
再レンダリングの問題
ただし、この方法では、文字を入力するたびにコンポーネントが再レンダリングされますの。頻繁な再レンダリングはパフォーマンスに影響を与える可能性がございますわ。その為に、useCallback
を使って関数をメモ化することが考えられますわ。
オブジェクトのstate更新
オブジェクトのstateを更新する場合、setState関数を使う際に、更新したい値だけではなくオブジェクト全体を指定する必要がございますわ。しかし、そのままですと冗長になるため、スプレッド構文を使用するとよりスマートに記述できますの。
import { ChangeEvent, useState } from "react";
const Lesson1_2 = () => {
const [form, setForm] = useState({
firstName: "Tarou",
lastName: "Kodai",
email: "kodaitarou@gmail.com",
});
return (
<div>
<div className="flex mb-5">
<label>
First Name:
<input
type="text"
className="border border-slate-500"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setForm({
firstName: e.target.value,
lastName: form.lastName,
email: form.email,
})
}
/>
</label>
<label>
Last Name:
<input
type="text"
className="border border-slate-500"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setForm({
firstName: form.firstName,
lastName: e.target.value,
email: form.email,
})
}
/>
</label>
<label>
Email:
<input
type="text"
className="border border-slate-500"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setForm({
firstName: form.firstName,
lastName: form.lastName,
email: e.target.value,
})
}
/>
</label>
</div>
<p>
{form.firstName}
<br />
{form.lastName}
<br />
{form.email}
</p>
</div>
);
};
export default Lesson1_2;
スプレッド構文
スプレッド構文を使用すると、以下のようにより簡潔に記述できますの。
<input
type="text"
className="border border-slate-500"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setForm({
...form,
firstName: e.target.value,
})
}
/>;
スプレッド構文で展開することで、変更したいオブジェクトの部分だけ、変更することができますわ。
アンチパターン
以下のコードは、ReactのuseState
フックを使用する際のアンチパターンを示しておりますの。
const[state, setState] = useState("");
state.name = "OTORI"
問題点
useState
で管理しているstate
を直接書き換えることは避けるべきですわ。なぜなら、state
は読み取り専用であり、直接書き換えるとReactのレンダリングの仕組みが崩れてしまうからですの。
ミューテーションとイミュータブル
このような直接的な変更をミューテーションと呼びますわ。反対に、元のデータをコピーして変更する方法をイミュータブルといいますの。
- ミュータブル: 直接的な変更
- イミュータブル: コピーしてから変更
JavaScriptでは、基本的にイミュータブルな操作を意識することが重要ですわ。直接的な変更は避け、新しい配列やオブジェクトをコピーしてから変更を加えるべきですの。
ミュータブルとイミュータブル
詳細な情報はこちらをご参照くださいませ
Updating Arrays in State - React
関数型プログラミングの重要性
関数型プログラミングでは、一度作成したデータを直接的に更新することを避け、新しいデータを作成してから変更することが基本ですの。これにより、純関数の特性を維持し、コードの予測可能性と信頼性を高めますわ。
onClickにおける関数の呼び出し方の違い
以下のコードにおいて、onClick
の呼び出し方には違いがございますわ。
<button onClick={() => handleClick()} className="border p-2 rounded-md bg-red-100">Add Age</button>
<button onClick={handleClick} className="border p-2 rounded-md bg-red-100">Add Age</button>
違いの説明
-
アロー関数を使用する場合:
<button onClick={() => handleClick()} className="border p-2 rounded-md bg-red-100" > Add Age </button>;
- ボタンがクリックされたときに、アロー関数が実行され、
handleClick
関数が呼び出されますの。 - この場合、アロー関数自体が新しい関数として定義されるため、コンポーネントが再レンダリングされるたびに新しい関数が作成されますの。
- ボタンがクリックされたときに、アロー関数が実行され、
-
関数を直接渡す場合:
<button onClick={handleClick} className="border p-2 rounded-md bg-red-100"> Add Age </button>;
- ボタンがクリックされたときに、直接
handleClick
関数が呼び出されますの。 - 再レンダリングが発生しても、
handleClick
関数は同じ参照を持ち続けますの。
- ボタンがクリックされたときに、直接
違いの影響
-
パフォーマンス:
- アロー関数を使用すると、再レンダリングごとに新しい関数が作成されるため、わずかにパフォーマンスに影響する可能性がございますわ。
- 直接関数を渡す場合は、再レンダリングが発生しても同じ関数参照が保持されるため、パフォーマンスへの影響は少ないですの。
-
使用する場面:
- アロー関数は、関数に引数を渡す必要がある場合や、特定のロジックを埋め込みたい場合に使用するのが良いですわ。
- 引数を必要としない場合や、パフォーマンスを重視する場合は、関数を直接渡す方が適していますの。
ReactのレンダリングがDOMに反映されるまでのフロー
Reactのレンダリングフローは一般的なブラウザのレンダリングとは異なりますの。
BrowserRendering
-
DOM Tree:
サーバーから送られたHTMLファイルを解析し、DOMツリーを構築しますの。 -
CSSOM Tree:
CSSオブジェクトモデルのツリーを構築しますの。 -
JavaScript:
JavaScriptをダウンロードし、ユーザーがインタラクティブな操作を行えるようにしますの。 -
Layout:
画面上にレイアウトを構成しますの。 -
Painting:
ブラウザ上に表示しますの。
ReactRendering
-
Trigger:
ユーザーの入力やイベントによりレンダリングが発火しますの。 -
Rendering the Component:
差分チェックを行いますの。実際のDOMを触らずに仮想DOMを操作し、以前の仮想DOMと現在の仮想DOMの差分をReactが検出し、変更を行いますの。💡 仮想DOMについてはこちらをご参照くださいませ:仮想DOMとは
-
Committing to the DOM:
実DOMに対して、変更内容を反映し、ブラウザに表示しますの。
このように、Reactは効率的なレンダリングを実現するために仮想DOMを使用し、差分のみを実DOMに反映する仕組みを持っておりますの。これにより、パフォーマンスが向上し、ユーザーの操作に迅速に応答できるようになっておりますわ。
仮想DOM
利点
仮想DOMの利点について詳しく説明いたしますわ。
-
効率的な更新:
実DOMを直接変更すると、変更箇所に関わらずその要素とその子ノードをすべて作り直す必要があり、これがパフォーマンスに大きな負荷をかけますの。仮想DOMは、差分をチェックして変更が必要な部分だけを実DOMに更新することで、この負荷を軽減しますわ。 -
高速な比較:
仮想DOMは軽量なJavaScriptオブジェクトであり、実際のDOMよりも比較が高速ですの。そのため、頻繁な更新や大量の要素を持つアプリケーションでも、効率的にレンダリングできますわ。 -
バッチ処理:
複数の変更をまとめて実DOMに適用することができますの。これにより、無駄な再描画を避け、パフォーマンスを向上させますの。
欠点
仮想DOMにも欠点がございますわ。
-
差分チェックの負荷:
アプリケーション全体の仮想DOMをチェックし、差分を見つけるプロセス自体に負荷がかかりますの。特に大規模なアプリケーションでは、このプロセスがパフォーマンスのボトルネックになることがありますわ。 -
必ずしもパフォーマンス向上にはならない:
仮想DOMを使用したからといって、必ずしもパフォーマンスが向上するわけではございませんの。状況によっては、仮想DOMの差分チェックがかえって負荷になる場合もありますわ。
まとめ
React Hooksの基本と重要性:
React Hooksとは、関数コンポーネントで状態管理や副作用の処理を行うための仕組みですわ。以前はクラスコンポーネントでこれらを実装しておりましたが、Hooksの導入により、よりシンプルで直感的なコードが書けるようになりましたの。
特に、API発火のタイミング制御などが必要な場合に、Hooksは非常に重要でございますわ。
useStateによる状態管理:
useState
フックを使用することで、状態変化に伴う再レンダリングが可能になりますの。直接変数を変更するのではなく、useState
を使って状態を管理することで、Reactの再レンダリングが正しく発生するのですわ。
特に、連続した状態更新の際には、関数を渡して前の状態を元に新しい値を計算する方法が推奨されますの。
レンダリングのタイミングとパフォーマンス:
Reactにおけるレンダリングは、初回描画時や状態変数が更新されたタイミングで発生しますの。不要な再レンダリングを防ぐために、useMemo
やuseCallback
を使用して、パフォーマンスを最適化することが重要ですわ。
Strictモードでは、純関数の特性を検証するためにコンポーネントが2回呼び出されることがございますの。
onChangeトリガーとオブジェクトのstate更新:
input
の値を状態として管理するには、onChange
トリガーを使用しますの。ただし、文字入力のたびに再レンダリングが発生するため、頻繁な更新はパフォーマンスに影響を与える可能性がございますわ。
オブジェクトのstateを更新する際には、スプレッド構文を使用することで、より簡潔に記述できますの。
仮想DOMの利点と欠点:
仮想DOMは効率的な更新と高速な比較が可能であり、複数の変更をまとめて実DOMに適用することでパフォーマンスを向上させますわ。
しかし、大規模なアプリケーションでは差分チェックのプロセス自体に負荷がかかることがあり、状況によっては仮想DOMの差分チェックがパフォーマンスのボトルネックになる場合もございますの。