どうも、ノマドクリエイターのショウヘイ(@shohei_creator)です。
ブログアシスタントのふーちゃんです。
React は、仮想 DOM を利用した差分検知によって、コンポーネントをレンダリングしています。レンダリングが実行されると、対象のコンポーネントは再構成されるため、コンポーネント内の変数も初期化されます。何か対策しなければ、更新内容の状態を保持できません。
React で状態の保持をおこなうには、クラスコンポーネントを使う必要がありました。しかし、 useState というフックが登場したことで、関数コンポーネントでも状態の保持が可能になりました。
この記事では、 React の useState フックの構文と具体例について説明します。
React の useState フックとは
React の useState フックは、再レンダリングされても、関数コンポーネントの state (変数の値)を保持しておけるフックです。
React によってコンポーネントが再レンダリングされると、コンポーネント内の変数は、初期値に戻されてしまいます。変数を保持しておきたい場合などは、クラスコンポーネントを使い、 state プロパティとして定義しておく必要がありました。
import React from "react";
type State = {
firstName: string;
lastName: string;
};
class UserName extends React.Component<{}, State> {
constructor(props: {}) {
super(props);
// ここで変数を保持している
this.state = {
firstName: "太郎",
lastName: "山田"
};
}
changeName = (): void => {
this.setState({
firstName: "花子",
lastName: "佐藤"
});
};
render() {
return (
<>
<p>
私の苗字は{this.state.lastName}で、名前は{this.state.firstName}です。
</p>
<button onClick={this.changeName}>名前を変える</button>
</>
);
}
}
export default UserName;
しかし、 useState フックが登場したことで、関数コンポーネントでも、 変数の状態を保持しておけるようになりました。クラスコンポーネントに比べて、より短く、より簡単にコードを書けます。
import React, { useState } from "react";
type State = {
firstName: string;
lastName: string;
};
export const UserName: React.VFC = () => {
// ここで変数を保持している
const [name, setName] = useState<State>({
firstName: "太郎",
lastName: "山田"
});
const changeName = (): void => {
setName({ firstName: "花子", lastName: "佐藤" });
};
return (
<>
<p>
私の苗字は{name.lastName}で、名前は{name.firstName}です。
</p>
<button onClick={changeName}>名前を変える</button>
</>
);
};
React の useState フックの基本構文
React の useState フックは、コンポーネントの中で使います。また、状態を保持するための state と state を更新するための setState と state の初期値の 3 要素が必要になります。
state と setState は、左辺に配列形式で書きます。変数名は、自由に名づけできます。なお、 setState は、「 set 」から書き始めることが慣習になっています。
state の初期値には、自由に値を設定して構いません。具体的な数字・文字はもちろん、空文字や null も設定できます。
こちらのコードは、 JavaScript で useState フックを書いた場合です。
import React, { useState } from "react";
export const Component = () => {
// 基本形
const [value, setValue] = useState(initialValue);
// 初期値に数字を設定した場合
const [number, setNumber] = useState(10);
// 初期値に文字列を設定した場合
const [string, setString] = useState("Hello, World!");
(以下、省略)
}
こちらは、 TypeScript で useState フックを書いた場合のコードです。 useState の直後で、型を宣言します。
import React, { useState } from "react";
export const Component: React.VFC = () => {
// 基本形
const [value, setValue] = useState<Type>(initialValue);
// 初期値に数字を設定した場合
const [number, setNumber] = useState<number>(10);
// 初期値に文字列を設定した場合
const [string, setString] = useState<string>("Hello, World!");
(以下、省略)
}
実際に useState フックを使う時は、 react ライブラリから import することを忘れないでください。
import React, { useState } from "react";
React の useState フックで複数の state を管理する方法
React の useState フックを使って、複数の state を管理する場合は、次の 2 択になります。
- state の数だけ useState フックを使う
- state をオブジェクト(連想配列)として定義する
state の数だけ useState フックを使う
React では、 1 つのコンポーネントの中で、何度も useState フックを使えます。複数の state を管理したい場合は、 state の分だけ useState フックを使えば OK です。
const [name, setName] = useState("山田太郎");
const [age, setAge] = useState(20);
const [gender, setGenger] = useState("男性");
const [name, setName] = useState<string>("山田太郎");
const [age, setAge] = useState<number>(20);
const [gender, setGenger] = useState<string>("男性");
state をオブジェクト(連想配列)として定義する
React の useState フックでは、 state にオブジェクト(連想配列)も定義できます。オブジェクトのプロパティとして、状態を保持したい変数を書いておきましょう。
const [user, setUser] = useState({
name: "山田太郎",
age: 20,
gender: "男性",
});
TypeScript を使う場合は、 type や interface を使って型定義しておき、 useState の 直後 で型宣言してください。
type User = {
name: string;
age: Agent;
gender: string;
};
const [user, setUser] = useState<User>({
name: "山田太郎",
age: 20,
gender: "男性",
});
React の useState フックで子コンポーネントに setState を受け渡しする方法
React の useState フックで作成される setState は、 props 経由で、親コンポーネントから子コンポーネントに受け渡せます。
子コンポーネント側で、 props 経由で受け取った setState を実行すると、親コンポーネント側の state が更新できることが確認できます。
import React, { useState } from "react";
import { Container, Alert } from "react-bootstrap";
import { Child } from "./Child";
export const Parent: React.VFC = () => {
const [state, setState] = useState<string>("親");
return (
<Container className="mt-4">
<Alert variant="primary">
<p>親コンポーネント</p>
<p>今のstateは、{state}です。</p>
</Alert>
// 子コンポーネントにsetStateを渡している
<Child setState={setState} />
</Container>
);
};
import React from "react";
import { Alert } from "react-bootstrap";
type Props = {
setState: React.Dispatch<React.SetStateAction<string>>;
};
export const Child: React.VFC<Props> = ({ setState }) => {
return (
<Alert variant="success">
<p>子コンポーネント</p>
<button
// 親コンポーネントのsetStateを実行
onClick={() => {
setState("子");
}}
>
stateを更新する
</button>
</Alert>
);
};
React の useState フックの状態が更新されない場合の原因
React の useState フックの仕様上、実装方法によって、 useState フックが期待通りに動作しないことがあります。
useState フックを使っても状態が更新されない場合の代表例をいくつか取り上げます。
React の useState フックで state が更新されるタイミングは、 setState の実行直後ではない
React の useState フックを実行すると、 state と setState を得られます。
setState に何かしらの値を渡して実行すると、すぐに state が更新されると思うかもしれません。しかし、これはよくある勘違いです。
setState を実行した段階では、まだコンポ―ネントの再レンダリングを予約しただけの状態です。
setState 関数は state を更新するために使用します。新しい state の値を受け取り、コンポーネントの再レンダーをスケジューリングします
フック API リファレンス - React
また、 setState は、非同期で実行されます。何かしらのイベントに紐づけて setState を実行しても、すぐに state が更新されるわけではありません。
setState 呼び出しは非同期です。呼び出し直後から this.state が新しい値を反映することを期待しないでください。
コンポーネントの state – React
上記の仕様が分かる具体例として、 setState を実行した直後に、アラートダイアログとして state の中身を表示するソースコードを用意しました。
ボタンをクリックしてみると、 setState を実行しても、アラートダイアログの中の数字は増えません。すぐに state が更新されていない証拠です。
import React, { useState } from "react";
export const Component: React.VFC = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prevCount) => prevCount + 1);
alert(count);
};
const decrement = () => {
setCount((prevCount) => prevCount - 1);
alert(count);
};
return (
<>
Count: {count}
<div>
<button onClick={increment}>1を足す</button>
<button onClick={decrement}>1を引く</button>
</div>
</>
);
};
原因: state を同インスタンスの配列・オブジェクト(連想配列)で更新しようとしている
React で useState フックを使う時に、 state として配列またはオブジェクト(連想配列)を設定する場合には、新規のインスタンスとして生成してから、 state を更新する必要があります。
React は、 Object.is メソッドを使って、 state が変更されたかどうか検知しています。
React は Object.is による比較アルゴリズムを使用します
フック API リファレンス - React
Object.is メソッドの仕様では、配列やオブジェクト(連想配列)を比較する時に、インスタンスを参照しています。
たとえば、配列の要素を操作できる push や splice などのメソッドを使うと、配列の中身は変わりますが、配列のインスタンスは変わりません。つまり、 React の差分検知に引っかからなくなり、 useState フックが機能しなくなります。
import React, { useState } from "react";
export const Component: React.VFC = () => {
const [array, setArray] = useState<string[]>(["Hello, "]);
const elChange = () => {
array.push("World!");
setArray(array);
};
return (
<>
<p>配列の中身: {array}</p>
<div>
<button onClick={elChange}>「World!」を追加する</button>
</div>
</>
);
};
対処法としては、新規インスタンスを生成する複製方式(ディープコピー)を使って、 setState に複製物を渡します。ディープコピーのやり方は色々とあるので、好みの方式を使ってください。
配列のディープコピーは、スプレッド演算子を使うと手軽です。
import React, { useState } from "react";
export const Component: React.VFC = () => {
const [array, setArray] = useState<string[]>(["Hello, "]);
const elChange = () => {
array.push("World!");
// スプレッド演算子を使ってarrayをディープコピー
// setArrayに渡して、arrayを更新
setArray([...array]);
};
return (
<>
<p>配列の中身: {array}</p>
<div>
<button onClick={elChange}>「World!」を追加する</button>
</div>
</>
);
};
オブジェクト(連想配列)のディープコピーは、 Object.assign メソッドを使うと手軽です。
import React, { useState } from "react";
type User = {
name: string;
age: number;
gender: string;
};
export const Component: React.VFC = () => {
const [user, setUser] = useState<User>({
name: "山田太郎",
age: 20,
gender: "男性"
});
const changeUser = () => {
// Object.assignを使って、userをディープコピー
let copyUser = Object.assign({}, user);
// copyUserの各種プロパティを更新
copyUser.name = "佐藤花子";
copyUser.age = 18;
copyUser.gender = "女性";
// copyUserをsetUserに渡して、userを更新
setUser(copyUser);
};
return (
<>
<p>名前: {user.name}</p>
<p>年齢: {user.age}</p>
<p>性別: {user.gender}</p>
<div>
<button onClick={changeUser}>人物を変更する</button>
</div>
</>
);
};