Reactの入門者がReact Hookを学び始めた時に最初に理解しなければならないのがuseState Hookです。useStateを使うとFunctionalコンポーネント(関数コンポーネント)内で状態管理を行うことができます。初めて状態管理という名前を聞く人にとってイメージが湧きにくい単語ですが要はコンポーネントの中で変数を扱うことができ変数に保存した値を保持することができる機能です。例えば入力フォームのinput要素に入力した文字をコンポーネント内で保持したい場合にuseState Hookを利用することができます。

useState Hookはコンポーネントの中で変数の値を保持、更新ができることからReactの中も最も使用頻度の高い機能なので簡単なサンプルを使って動作確認を行なっていきます。

useState Hookを使いこなさなければFunctionalコンポーネントでアプリケーションを作成することはできません。
fukidashi

シンプルな例を通してuseStateの理解

useStateの設定

コンポーネント内でuseStateを利用するためには、useStateをimportする必要があります。


import { useState } from 'react';

コンポーネント内で状態管理として利用したい変数をuseStateを使って宣言します。初めて使用する人であれば最初は書式に違和感のある方も多いもしれませんが使えばすぐに慣れるものです。まず最初にuseStateの書式をしっかりと覚えてください。

importしたuseState Hookの引数に初期値を設定します。useStateの戻り値は配列で一つ目の要素には状態(変数)が戻され、二つ目の変数には状態を更新するための関数が戻されます。下記の例ではuseState関数から戻されるcountが変数となりsetCount関数を利用してcountの値を更新することができます。countの初期値は0です。


const [count, setCount] = useState(0);

上記の設定を行うだけでコンポーネント内のJSX関数に{ count }を追加するとcountの現在値を表示することができます。初期値は0に設定し、そこから何も変更を行っていないためブラウザ上には初期値である0が表示されます。初期値の値を変更するとブラウザ上に表示される値も変わります。


import { useState } from 'react';

function Count() {

    const [count, setCount] = useState(0);

    return (
        <div>
            <h1>Counter</h1>
            <h2>カウント: { count }</h2>
        </div>
  );
}

export default Count;
countの値を表示
countの値を表示

countの値の更新

先程setCount関数でcountの値を更新できると説明しました。どのようにcountの値を更新するのか実際にコードを使って確認していきます。

クリックしたらcountの値が更新できるようにボタンを追加しonClickイベントを追加します。onClickの中でsetCount関数を利用してcountの値を更新します。


<button onClick={() => setCount(count + 1)}>+</button>

countを1増やすだけではなく-1に減らすボタンも追加します。


import { useState } from 'react';

function Count() {

  const [count, setCount] = useState(0);

  return (
      <div>
          <h1>Counter</h1>
          <h2>カウント: { count }</h2>
          <button onClick={() => setCount(count + 1)}>+</button>
          <button onClick={() => setCount(count - 1)}>-</button>
      </div>
  );
}

export default Count;

+ボタンをクリックすると1増え、-ボタンをクリックすると1減るカウンターが作成できました。ボタンを押すとコンポーネントの内で行ったカウントの増減が即座にブラウザ上にも反映されます。

クリックでCountの値が増減
クリックでCountの値が増減
コンポーネントのsetCountでcountを更新したらブラウザ上にも反映されるのが不思議だと感じる人もいるかもしれません。即座に反映されるのはuseStateで変数の値を更新する度にコンポーネントが再実行され、ブラウザ上で変数の再描写が行われているためです。ReactではuseStateによる変数の更新だけではなくpropsが更新されてもコンポーネントの再描写が行われるということを理解しておく必要があります。再描写を行う仕組みがない場合はstateを更新しても最新の情報がブラウザに表示されることはありません。React HooksのuseCallback, useMemo, useRefやmemo等の機能がコンポーネントの再描写に関係しています。
fukidashi

onClickで実行している関数を外側に定義しても動作は変わりません。このようにuseStateを利用するとコンポーネント内で値を保持したり更新したりすることができます。


import { useState } from 'react';

function Count() {

    const [count, setCount] = useState(0);

    const increment = () => setCount(count + 1)
    const decrement = () => setCount(count - 1)

    return (
        <div>
            <h1>Counter</h1>
            <h2>カウント: { count }</h2>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
        </div>
  );
}

export default Count;

更新は非同期

increment関数を使うことでcountの数を1増やすことは可能です。しかしincrement関数の中で増やしたcountで何か処理をしようとしても更新は非同期なため更新した後のcountの値を取得することはできません。


const increment = () => {
  setCount(count + 1);
  console.log(count);
};

+ボタンを押すとコンソールには1増えたcountが表示されると思いますが実際にはcountは即座に更新されていないためボタンを押しても数は増えません。

下記のように+ボタンを押すとブラウザ上のカウントは1増えますがコンソールログのcountは0のままです。

countは即座に更新されない
countは即座に更新されない

countがsetCountで再描写が行われた後にはcountは更新されていることが確認できます。再描写されたことがわかるようにコードを追加します。


import { useState } from 'react';

function Count() {
  const [count, setCount] = useState(0);

  console.log('再描写');
  console.log(count);

  const increment = () => {
    setCount(count + 1);
    console.log(count);
  };
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <h1>Counter</h1>
      <h2>カウント: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default Count;

ページを開くと再描写ではありませんが初めてコンポーネントが実行されるのでコンソールには再描写と0が表示されます。

最初の描写
最初の描写

ボタンをクリックします。incremant関数内ではcountは更新されていませんが再描写した後はcountが更新されていることが確認できます。

再描写後はcount更新
再描写後はcount更新

useStateでbooleanを使う

状態管理を行う変数の値にbooleanを使用することができます。booleanのtrueとfalseを利用することでコンポーネントの表示・非表示を切り替える方法はReactアプリケーションのさまざまな場所で利用されています。

ONボタンとOFFボタンで電源が切り替わるPowerコンポーネントを作成します。


import { useState } from 'react';

function Power() {

    const [power, setPower] = useState(false);

    return (
        <div>
            <h1>電源 { power ? 'ON' : 'OFF'} </h1>
            <button onClick={() => setPower(true)}>ON</button>
            <button onClick={() => setPower(false)}>OFF</button>
        </div>
  );
}

export default Power;

useStateで初期値をtrueまたはfalseに設定することができます。上記ではuseStateでfalseに設定しているのでpowerの初期値はfalseになります。

ONボタンとOFFボタンを用意しonClickイベントを使ってpowerの値をtrueまたはfalseに変更できるように設定します。

ブラウザで確認すると下記のように表示され、ONボタンを押すと電源ONと表示され、OFFボタンを押すと電源OFFと表示されます。

電源のON, OFFを切り替え
電源のON, OFFを切り替え

前の値を利用して切り替える

ONとOFFを切り替えるのに2つのボタンを用意していましたが、useStateでは現在の値を元にして新しい値を設定することができます。現在の値を利用したい場合にはsetPower関数の中で関数を利用します。prevStateがボタンをクリックする前の値でボタンをクリックするとprevStateの値で別の値に変わります。つまり現在の値がtrueであればfalse、falseであればtrueに変わります。


<button onClick={() => setPower(prevState => !prevState)}>ON/OFF</button>

prevStateとしていますが、名前は任意なのでどのような名前でもつけることが可能です。

ボタンを1つにする
ボタンを1つにする

ボタンをクリックするたびにONとOFFが切り替わります。コンポーネントの表示・非表示を行うToggle機能もこのようにuseState Hookの値を利用して実装することができます。

Vue.jsを学習してきた人であればbooleanによる条件分岐にはv-ifディレクティブ, v-showディレクティブを利用するかと思います。Reactでは条件分岐にはJavaScriptの機能を利用します。Reactでは条件分岐に独自の機能(Vue.jsではディレクティブ)はありません。
fukidashi

useStateを利用してフォーム

コンポーネント内でのuseState Hookは入力フォームで利用する機会が多いのでシンプルなフォームを作成を通してさらにuseStateの理解を深めていきましょう。

これからuseState Hookの入力フォームでの利用方法を確認していきますが、公式の旧ドキュメントのuseStateのNoteの下記の画像に記載されている説明の意味がわからない場合はこれから行っていく説明が理解の助けになるかと思います。

useStateのNote
useStateのNote

メールアドレスとパスワードの入力項目を備えたフォームを作成します。emailとpasswordはuseStateを利用して入力の値を取得します。input要素からはイベントを利用して入力した値を取得しています。


import { useState } from 'react';

function App() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const handleSend = (e) => {
    e.preventDefault();
    console.log(email, password);
  };
  return (
    <div style={{ textAlign: 'center', marginTop: '2em' }}>
      <h1>useStateでフォーム</h1>
      <form onSubmit={handleSend}>
        <div>
          <label>
            メールアドレス:
            <input
              name="email"
              type="email"
              onChange={(e) => setEmail(e.target.value)}
            />
          </label>
        </div>
        <div>
          <label>
            パスワード:
            <input
              name="password"
              type="password"
              onChange={(e) => setPassword(e.target.value)}
            />
          </label>
        </div>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

export default App;

ブラウザで確認するとメールアドレスとパスワードにの入力を行いSubmitボタンを押すと入力した情報が表示されます。

入力フォームを表示
入力フォームを表示

ここまでのuseState Hookの使い方が理解できていれば難しいところはないかと思います。

emailとpasswordを別々の変数にわけて設定を行うのではなくformオブジェクトをuseState Hookの初期値として設定します。先程までの方法ではemail、password以外に入力項目が増えてたら増えた分だけuseStateで新たに変数を定義する必要があります。しかしformオブジェクトを利用した場合は初期値の中のオブジェクトに入力項目のプロパティを追加する必要があります。


const [form, setForm] = useState({ email: '', password: '' });

ではオブジェクトを利用した場合はどのようにformオブジェクトの値を更新することができるのでしょうか?

input要素にはname属性を設定していたのでname属性を利用してどちらの入力項目が入力されたかを確認することができます。


[e.target.name]: e.target.value

formオブジェクトを更新したい場合には更新前のprevStateとspread operatorsを利用して下記のように行うことができます。


setForm((prevState) => {
  return {
    ...prevState,
    [e.target.name]: e.target.value,
  };
});

formオブジェクトを利用すると全体のコードを下記のように変更となります。onChageイベントはname属性でどの項目が入力されたか判定できるため同じhandleChage関数を設定しています。


import { useState } from 'react';

function App() {
  const handleSend = (e) => {
    e.preventDefault();
    console.log(form);
  };
  const [form, setForm] = useState({ email: '', password: '' });
  const handleChange = (e) => {
    setForm((prevState) => {
      return {
        ...prevState,
        [e.target.name]: e.target.value,
      };
    });
  };
  return (
    <div style={{ textAlign: 'center', marginTop: '2em' }}>
      <h1>useStateでフォーム</h1>
      <form onSubmit={handleSend}>
        <div>
          <label>
            メールアドレス:
            <input
              name="email"
              type="email"
              onChange={handleChange}
            />
          </label>
        </div>
        <div>
          <label>
            パスワード:
            <input
              name="password"
              type="password"
              onChange={handleChange}
            />
          </label>
        </div>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

export default App;

ブラウザで動作確認をしてもemailとpasswordで変数を別々に定義した時と結果は同じです。

setFormのコードについては下記のようによりシンプルに記述することができます。


setForm({
  ...form,
  [e.target.name]: e.target.value,
});

またObject.assignを利用することもできます。


setForm(Object.assign(form, { [e.target.name]: e.target.value }));

ここまでの説明が理解できればuseStateを使いこなすことが可能です。

Reactのフォームについてもう少し詳しく知りたい場合は下記の記事が参考になります。