React を使う上では必須となる知識、それが「React Hooks」です。
今回はその中でも特によく使われ、かつアプリの使い心地を向上させる状態管理の Hooks である useState について詳しく解説していきます。
React Hooksとは?
React Hooksは、Reactの大きな進化の一つです。
Hooks は関数コンポーネントという種類のコードに、追加の機能を与えるために使います。
例えば、以前までの関数コンポーネントでは、データの保存や変更といった機能を持つことができませんでした。
しかし、Hooksの登場により、Redux
などのライブラリを使わずに、関数コンポーネントでもデータの保存や変更ができるようになりました。
※関数コンポーネントとは、以下のようなコードのことです。
import React from 'react';
function Example() {
return (
<div>
<p>Hello, world!</p>
</div>
);
}
Reactにはもう一つのコードの書き方として、クラスコンポーネントがあります。
クラスコンポーネントでは、データの保存や変更といったことができます。
※クラスコンポーネントとは、以下のようなコードのことです。
import React from 'react';
class Example extends React.Component {
render() {
return (
<div>
<p>Hello, world!</p>
</div>
);
}
}
しかし、クラスコンポーネントは関数コンポーネントに比べてやや複雑です。
そこで、ReactチームはHooksを導入し、関数コンポーネントでもクラスコンポーネントと同じ機能が使えるようにしました。
これにより、関数コンポーネントの使い勝手が大きく向上したため、現在ではHooksを使った関数コンポーネントが主流になっています。
Hooksはいくつかの種類がありますが、その中で最も基本的なのがuseStateです。
ReactのuseStateとは?
useStateは、その名の通り、“状態(state)”を使うためのHookです。
まず「状態」とは何かを説明しましょう。
Webページを見るとき、ボタンを押すと画面が変わったり、テキストを入力すると内容が変わったりすることがありますよね。
これは、画面がある「状態」から別の「状態」に変わるということを示しています。
useStateは、そのような状態を扱うためのものです。
例えば、ボタンが押された回数を数えるための状態や、テキストボックスに入力されたテキストを保存するための状態など、様々な「状態」を管理することができます。
useStateの使い方
基本的なuseStateの使い方を学んでみましょう。
ここでは、ボタンが押された回数を数えるためのコードを作ります。
まず、useStateを使うためには以下のように書きます。
import React, { useState } from 'react';
// ...コンポーネント内で以下のように書く
const [count, setCount] = useState(0);
この一行のコードで2つのことが行われています。
まず、useState(0)の部分で、「ボタンが押された回数」という状態を作り、初期値を0に設定しています。
そして、その状態を操作するための2つの変数、count
とsetCount
を作っています。
count
は現在の「ボタンが押された回数」の状態を表します。
そしてsetCount
は「ボタンが押された回数」の状態を変更するための関数です。
これを見た時、変数の扱いに慣れている方は「 count
を直接書き換えれば良いのでは?」と思うかもしれません。
しかし、count
を直接書き換えても、画面に反映されないというのがReactの特徴です。
Reactでは、状態を変更するためには必ずsetCount
関数を使う必要があります。
setCount
関数を呼び出して、「ボタンが押された回数」の状態を変更します。return (
<div>
<button onClick={() => setCount(count + 1)}>ここをクリック</button>
</div>
);
そして、ボタンが押された回数を表示するためには、以下のようにcount
変数を使います。
return (
<div>
<p>ボタンが押された回数: {count}</p>
<button onClick={() => setCount(count + 1)}>ここをクリック</button>
</div>
);
このコードを実行すると、「ボタンが押された回数」を表示するページが作られ、ボタンを押すごとにその回数が1ずつ増えていくことがわかります。
このように、useStateを使うと、ページ上の情報の「状態」を管理し、それに基づいて画面の表示を更新することができます。
useStateの仕組み
useStateがどのように働いているかを簡単に説明します。
useStateは、現在の状態と、その状態を更新するための関数をペアにして提供します。
これにより、関数コンポーネント内で状態を保持し、それに基づいて何かを行うことができます。
また、useStateの更新関数(上記の例ではsetCount
)を呼び出すと、Reactは新しい状態でコンポーネントを再レンダリングします。
このため、状態が更新されるたびに、新しい状態に応じた画面がユーザーに表示されます。
なお、状態を更新する際には、新しい状態の値を直接指定することもできますし、現在の状態から新しい状態を計算する関数を指定することもできます。
これは特に状態の更新が複数回にわたって行われる場合や、現在の状態に基づいて新しい状態を決定する場合に有用です。
例えば、以下のように書くことができます。
setCount(prevCount => prevCount + 1);
ここでのprevCount
は更新前の状態(つまり、「ボタンが押された回数」)を示しています。prevCount + 1
でその回数に1を足した値が新しい状態となります。
useStateを複数使う
一つのコンポーネントの中で、useStateを複数回使うこともできます。
それぞれのuseStateは独立した状態を持ちます。
例えば、ユーザーの名前と年齢を管理する状態を作ることができます。
import React, { useState } from 'react';
function ExampleWithManyStates() {
const [age, setAge] = useState(20);
const [name, setName] = useState('鈴木');
return (
<div>
<p>こんにちは、私は {name} です。{age} 歳です。</p>
<button onClick={() => setAge(age + 1)}>年を取る</button>
<button onClick={() => setName('佐藤')}>名前を「佐藤」に変える</button>
</div>
);
}
export default ExampleWithManyStates;
上記のコードでは、useStateを2回呼び出して2つの状態、age
とname
を作っています。
そして、それぞれの状態を更新するためのボタンを作りました。
これらのボタンを押すと、それぞれの状態が更新され、画面の表示もそれに合わせて更新されます。
useStateで配列を取り扱う
useStateは、単純な数値や文字列だけでなく、配列やオブジェクトなどの複雑なデータを扱うこともできます。
例えば、以下のコードは、ToDoリストを作るための状態を作ります。
const [todos, setTodos] = useState(['牛乳を買う', 'ジムに行く']);
このように、配列を使った状態を作ることで、複数の項目を一つの状態として管理することができます。
配列の状態を更新するには、新しい配列を作ってそれをセットします。
以下の例では、「掃除をする」をToDoリストに追加するためのボタンを作ります。
<button onClick={() => setTodos([...todos, '掃除をする'])}>Todo追加</button>
[...todos, '掃除をする']
という部分で、既存のToDoリスト(todos
)に新しいタスク(’掃除をする’)を追加した新しい配列を作っています。
そして、setTodos
関数を使ってその新しい配列を状態にセットしています。
[...todos, '掃除をする']
という書き方は、配列のスプレッド構文というものです。スプレッド構文は、配列やオブジェクトの中身を展開して新しい配列やオブジェクトを作るためのものです。ここで、元の配列に新しい要素を追加する方法としては
push
メソッドを使うことも出来る、と思うかもしれません。しかし、
push
メソッドは元の配列を変更してしまうため、Reactの状態を更新する際には使うべきではありません。なぜなら、Reactは状態が更新されたかどうかを「状態の値が変わったかどうか」で判断し、状態が変わった場合にだけ画面を更新するからです。これらをまとめると、以下のようなToDoリストアプリのコードになります。
import React, { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState(['牛乳を買う', 'ジムに行く']);
return (
<div>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={() => setTodos([...todos, '掃除をする'])}>Todo追加</button>
</div>
);
}
export default TodoApp;
このコードを実行すると、ToDoリストを表示するページが作られ、”Todo追加”ボタンを押すたびに新しいタスクがリストに追加されます。
ステートとコンポーネント
useStateを使うことで、Reactのコンポーネントが動的になります。
つまり、ユーザーの操作によってコンポーネントの表示が変わるようになります。
ステートは、ユーザーがアプリケーションとどのように対話しているかを表すデータです。
例えば、テキストボックスの内容、チェックボックスがチェックされているかどうか、表示するリストの項目など、すべてがステートと呼べます。
ステートの値が変わると、そのステートに依存するコンポーネントが再レンダリングされます。
つまり、ステートの変更が直接画面の更新を引き起こします。
useStateの注意点
useStateを使うときには、いくつか注意点があります。
同期的にステートを更新しない
Reactのステート更新は非同期で行われることを覚えておきましょう。
つまり、ステート更新関数を呼び出した直後にステートの値を読み出そうとしても、まだ更新されていない可能性があります。
そのため、ステート更新関数を呼び出した直後にステートの値を依存にする処理を書くことは避けるべきです。
NGな例:
const [count, setCount] = useState(0);
// NG: ステート更新関数を呼び出した直後にステートの値を読み出している
setCount(count + 1);
上記の例だと、setCount
関数を呼び出した直後にcount
の値を読み出しています。
そのため、count
の値は更新されていない可能性があります。
正しくは、ステート更新関数に関数を渡して、その中でステートの値を読み出すようにします。
OKな例:
const [count, setCount] = useState(0);
// OK: ステート更新関数に関数を渡して、その中でステートの値を読み出している
setCount(prevCount => prevCount + 1);
ステート更新関数に関数を渡すと、その関数の引数に現在のステートの値が渡されますので、その値を使ってステートの更新を行います。
少しややこしいですが重要な概念なので、しっかりと理解しておきましょう。
ループや条件文内でuseStateを呼び出さない
React Hooksはループや条件文、入れ子になった関数の中から呼び出すべきではありません。
それらの中でHooksを呼び出すと、Reactが内部で管理しているHooksの呼び出し順序が乱れてしまい、思わぬバグを引き起こす可能性があります。
NGな例:
function TodoApp() {
const [todos, setTodos] = useState(['牛乳を買う', 'ジムに行く']);
if (todos.length > 0) {
const [todo, setTodo] = useState(todos[0]);
}
return (
<div>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={() => setTodos([...todos, '掃除をする'])}>Todo追加</button>
</div>
);
};
上記の例では、if
文の中でuseState
を呼び出していますが、これだとReactが内部で管理しているHooksの呼び出し順序が乱れてしまいます。
このとき、if
文の中のuseState
が先に呼び出されてしまうと、todos
の値が更新されてしまい、todos
の値が更新されたことによって再レンダリングが発生してしまいます。
このように、Hooksの呼び出し順序が乱れると思わぬバグを引き起こす可能性があるので、ループや条件文、入れ子になった関数の中からHooksを呼び出すべきではありません。
OKな例:
function TodoApp() {
const [todos, setTodos] = useState(['牛乳を買う', 'ジムに行く']);
const [todo, setTodo] = useState(todos[0]);
return (
<div>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={() => setTodos([...todos, '掃除をする'])}>Todo追加</button>
</div>
);
};
上記の例では、if
文の中でuseState
を呼び出していないので、Hooksの呼び出し順序が乱れることはありません。
useStateの初期値とパフォーマンス
useStateの初期値は、コンポーネントが最初にレンダリングされるときだけに計算されます。
しかし、初期値の計算に時間がかかる場合やリソースを多く使う場合には、初期値を関数として提供することでパフォーマンスを改善することができます。
例えば、以下のようなコードがあったとしましょう。
const [value, setValue] = useState(expensiveComputation());
このコードでは、expensiveComputation()
関数が計算に時間がかかる処理だとします。
useState は再レンダリングのたびに呼び出されるので、このコードでは毎回expensiveComputation()
関数が呼び出されてしまい、パフォーマンスが悪くなってしまいます。
このような場合には、以下のように初期値を関数で提供することが推奨されます。
const [value, setValue] = useState(() => expensiveComputation());
このように書くと、expensiveComputation()
は初期レンダリングのときだけ呼び出され、その結果が状態の初期値として使用されます。
そのため、再レンダリングのたびにexpensiveComputation()
が呼び出されることがなく、パフォーマンスが改善されます。
おさらい
ReactのuseStateは、状態管理のための強力な道具です。
この記事で紹介したように、useStateを使って値を保持し、その値が変わるたびにコンポーネントが更新されることを理解することが重要です。
useStateは独立した値を持つため、複数の状態を一つのコンポーネントで管理することができます。
また、配列やオブジェクトなどの複雑なデータを扱うことも可能です。
それぞれのuseStateは独立したステートを作りますので、異なる値を保持したい場合にはuseStateを複数回呼び出すことが可能です。
ただし、Reactのステート更新は非同期で行われるため、ステート更新関数を呼び出した直後にステートの値を読み出そうとしても、まだ更新されていない可能性があります。
また、useStateを呼び出す際にはループや条件文、入れ子になった関数の中から呼び出さないように注意しましょう。
これらの中でuseStateを呼び出すと、Reactが内部で管理しているHooksの呼び出し順序が乱れてしまい、思わぬバグを引き起こす可能性があります。
最後に、初期値の計算に時間がかかる場合やリソースを多く使う場合には、初期値を関数で提供することでパフォーマンスを改善することができます。
このような知識を持っていれば、ReactのuseStateを最大限に活用することができます。
Reactの状態管理は間違えるとパフォーマンスに影響を与えてしまうなど、難しい部分もあります。
しかし、この記事で紹介した知識をしっかりと理解しておけば、Reactの状態管理をスムーズに行うことができるでしょう。
React を効率的に身につける勉強法は?
React には興味があっても、プログラミング学習をはじめたばかりであれば、以下のような疑問を持つかもしれません。
- Reactエンジニアとして活躍するために何をすればいい?
- 何から勉強すればいいんだろう?
- 絶対に転職を成功させるには?
React は習得が比較的難しいため、勉強法の選び方を間違うと時間・労力・お金が無駄になってしまいます…。
そのため、こちらの記事を参考に、ご自身に最適な学習方法を選んでみませんか?