その他

【React】状態管理手法について、使い分けの考え方を解説

その他
この記事は約14分で読めます。

インプル品質管理チームの泉本です。

Reactの状態管理について、様々な知見がネットの海には散見されます。
そんな中、何を基準に状態管理の方法を選べばいいのかわからない人も多いと思います。

本記事では、Reactの状態管理でよく話題に上がる(と思っている)「useState」「Redux」「react-hook-form」「formik」に焦点を当て、それぞれどの場面での状態管理に適しているのか解説していきます。
なお、本記事内では「状態管理」に焦点を当てている為、アプリの軽量化やレンダリングの最適性などは考慮しておりません。ご留意ください。

「状態管理」とは?

そもそもReactにおける「状態管理」とは何かについて簡単に説明いたします。
状態管理とは、アプリケーション内で変化するデータを管理することを指します。

Reactでは、コンポーネントという概念があります。コンポーネンとは、UIを構築するために使用される部品のようなもので、そのコンポーネントそれぞれが、自身が表示するデータを管理しています

例えばボタンのコンポーネントで保持しているデータは、タイトルを表示するコンポーネントではアクセスできず、逆もまた然りです。

この個々のコンポーネントが持つデータのことを内部状態と言います。
この内部状態を適切に管理し、アプリケーションの振る舞いを制御することを状態管理と言います。

useState

useStateはreactが提供するフックの一つで、Reactコンポーネント内で状態を管理するための変数と、その変数を更新するための関数を使用することができます。
useStateを用いることで、従来の状態管理より、簡単に状態管理を行うことができます。

「クリックすると数字を1増やすボタン」をuseStateを用いて実装する例と、useStateを使わないで状態管理をする例を見てみましょう。

useStateを用いる場合

import React, { useState } from 'react';

const Example = () => {
  // 初期値が0のcountという変数と、それを更新するため関数setCountを用意
  const [count, setCount] = useState(0);

  // クリック時に呼び出される関数
  const handleIncrement = () => {
    setCount(count + 1); // 現在のcountに1を追加して状態を更新する
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleIncrement}>+1</button>
    </div>
  );
}

useStateを用いない場合

import React, { Component } from 'react';

class Example extends Component {
  // constructorメソッドで状態の初期値を設定する
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    this.handleIncrement = this.handleIncrement.bind(this);
  }

  // handleIncrementメソッドで状態を更新
  handleIncrement() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>カウント: {this.state.count}</p>
        <button onClick={this.handleIncrement}>+1</button>
      </div>
    );
  }
}

useStateなどを用いない場合、クラスコンポーネントを使用することになります。
比べてみると一目瞭然ですが、useStateを用いるととてもシンプルで、直感的な実装になることがわかります。

useStateは、ある一定の動作をした場合にアラートを出したい、といった際のフラグ管理や、その画面内でしか使わないデータの管理を行うのに適しています。また、小規模なアプリケーションであれば、useStateで管理したデータをpropsとしてバケツリレー式に渡す設計でも実装が行えるかもしれません。

Redux

Reduxは、JavaScriptアプリケーションのための状態管理ライブラリです。
JavaScriptのためのライブラリのため、React以外でも使用することが可能です。

Reduxは、個々のコンポーネントで管理していたデータを、Storeと呼ばれる場所に保存します。このStoreは、どのコンポーネントからでもアクセスすることが可能で、アプリケーション全体で同じデータを扱うことができるため、個々で状態管理をするよりもデータを一貫して持つことが可能です。

Reduxを使用するためには、以下の三つの要素が必要になります。

Storeアプリケーションのデータを管理するオブジェクト
ActionReducerで定義した、どの関数を呼び出すかを定義するためのオブジェクト。typeとpayloadの二つの要素から定義する。
ReducerStoreの内容を更新するための関数。Actionの内容に応じて、Storeに保存されているデータを更新する

複数画面で同じデータを扱いたい場合や大規模システムの開発に、Reduxは適しています。
例えば、APIから取得した商品データをセレクトボックスの選択肢にしたい場合など、各コンポーネントごとにAPIを叩いて商品データを取得して、それを個々のコンポーネントごとに状態管理してセレクトボックスの選択肢にするのは少々手間がかかります。

Reduxを使用することで、一度取得した商品データをどのコンポーネントでも使用することが可能になるため、データに一貫性ができるのと同時に、管理が簡単になります。

React Hook Form

React Hook Formとは、厳密には状態管理を行うためのライブラリではなく、フォームのバリデーションなどを行いやすくするためのライブラリになります。ですが、React Hook Formを使う場合は、useStateなどのReactの状態管理の方法を使用する必要はなく、React Hook Formが提供する仕組みで状態管理を行うことができます。

React Hook Formはフォームの状態管理、バリデーションを扱うのに適しています。

名前とフリガナを入力する簡単なフォームを例に、React Hook Formを使用した例と、useStateで管理する例を見てみましょう。

React Hook Formを用いる場合

import React from "react";
import { useForm } from "react-hook-form";

const Form = () => {
  const { register, handleSubmit, formState: { errors } } = useForm({
    // どのタイミングでバリデーションチェックを行うかを指定(デフォルトがonSubmitなので書かなくても良い)
    mode: "onSubmit",
    // 初期値を設定
    defaultValues: {
      name: "",
      kana: ""
    }
  });

  /**
    送信ボタンを押した際に発火する関数。
    この関数の発火前に、modeでonSubmitを指定しているため、この関数が発火する前にバリデーションチェックが行われる
    dataにはReact Hook Formで状態管理しているデータがobjectの形で入っている
  */
  const showConsoleLog = (data) => {
    console.log("name =", data.name, ", kana =", data.kana);
  };

  return (
    <form onSubmit={handleSubmit(showConsoleLog)}>
      <label>
        名前:
        <input
          type="text"
          {...register("name")}
        />
      </label>
      <br>
      <label>
        フリガナ:
        <input
          type="text"
          {...register("kana", { pattern: /^[ァ-ヶー  ]+$/ })}
        />
        // 送信ボタン押下時、バリデーションエラーが発生した場合errorsにエラー状態が保持される。
        {errors.kana?.type === "pattern" && <p>カタカナで入力してください</p>}
      </label>
      <br>
      <button type="submit">送信</button>
    </form>
  );
};

上記の例では、useStateを利用した時のように、状態管理しているデータを更新する関数は特に記述していませんが、WEB上で値を更新した場合は、名前、フリガナ共に変更が保持されます。
また、バリデーションも指定しているため、カタカナ以外の入力の場合は、ボタン押下時にエラーが表示されます。

useStateを用いる場合

では、React Hook Formを使用しないで、useStateによる状態管理をした場合にどうなるかをみてみましょう。

import React, { useState } from "react";

const Form = () => {
  const [name, setName] = useState("");
  const [kana, setKana] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const handleSubmit = (e) => {
    // submitのデフォルトのイベントをキャンセル
    e.preventDefault();
    // バリデーションチェックを行う。 
    if (!/^[ァ-ヶー  ]+$/.test(kana)) {
      // カタカナ以外が入力されていた場合エラーメッセージをセットする
      setErrorMessage("カタカナで入力してください");
    } else {
      // 問題ない入力の場合はエラーメッセージを空にし、console.logでデータを出力
      setErrorMessage("");
      console.log("name =", name, ", kana =", kana);
    }
  };

  // 入力時にnameの状態を変更する関数
  const changeName = (e) => {
    setName(e.target.value)
  }

    // 入力時にkanaの状態を変更する関数
  const changeKana = (e) => {
    setKana(e.target.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        名前:
        <input
          type="text"
          value={name}
          onChange={changeName}
        />
      </label>
      <br>
      <label>
        フリガナ:
        <input
          type="text"
          value={kana}
          onChange={changeKana}
        />
      </label>
      <br>
      <button type="submit">送信</button>
      {errorMessage && <p>{errorMessage}</p>}
    </form>
  );
};

React Hook Formを使った場合と比べて、onChangeのタイミングで状態を変更するための関数を定義したり、エラーメッセージを用意するための変数を用意したりと、管理が若干複雑になっています。
また、バリデーションの確認も独自で行う必要があるため、コードの総量も多くなってしまいます。

実際に開発を行う際には、フォームの管理をReact Hook Formで行い、確認画面や完了画面などに情報を引き継ぎたい場合にReduxなどを用いて他の画面でも入力情報を使えるように管理する、といった運用で用いられることもあります。

Formik

FormikもReact Hook Formと同様に、Reactのフォーム処理を簡素化するためのライブラリで、フォームの状態管理、バリデーション、サブミットの処理などをカスタマイズして使用することが可能です。

Formikの使用例

import React from 'react';
import { useFormik } from 'formik';

const validate = values => {
  const errors = {};

  if (!/^[ァ-ヶー  ]+$/.test(values.kana)) {
    errors.kana = 'カタカナで入力してください';
  }

  return errors;
};

const Form = () => {
  const formik = useFormik({
    initialValues: {
      name: '',
      kana: '',
    },
    // validationのルールを指定。別ファイルから読み込んでもOK
    validate,
        // submit時の挙動
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2));
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <div>
        <label htmlFor="name">名前</label>
        <input
          id="name"
          name="name"
          type="text"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.name}
        />
        {formik.touched.name && formik.errors.name ? (
          <div>{formik.errors.name}</div>
        ) : null}
      </div>
      <div>
        <label htmlFor="kana">カタカナ</label>
        <input
          id="kana"
          name="kana"
          type="text"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.kana}
        />
        {formik.touched.kana && formik.errors.kana ? (
          <div>{formik.errors.kana}</div>
        ) : null}
      </div>
      <button type="submit">送信</button>
    </form>
  );
};

React Hook Formと比較すると少し冗長な感じを受けるかもしれません。
しかし、Formikではyupと呼ばれるバリデーションライブラリを使用することで、詳細なバリデーションを実装することが可能です。(状態管理という本筋から外れてしまうので本記事ではこれ以上解説しません)

まとめ

簡単ではありますが、各状態管理手法のサンプルコードや、適した使用場面について説明を行いました。まとめると以下のような使い分けを行うと状態管理が行いやすくなるかと思います。

状態管理手法使用用途具体例
useStateその画面限りの簡単なデータ管理
小規模なシステムで、Reduxを使うほどの規模でない場合
「ダイアログを表示するか否か」などのフラグ管理
Redux 複数画面で同じデータを扱いたい時
大規模なシステム界の場合
APIから取得したデータなどを複数画面で使用したい場合
入力画面で入力したデータを確認画面や完了画面に引き継ぎたい場合
React Hook Form
Formik
フォームのデータ管理顧客情報の入力画面など、入力フォーム

もちろんこれは一例に過ぎず、柔軟に使い分けを行うことでシステム開発をより簡単に行うことができるようになります。実装したいシステムに沿った状態管理を行うことで、不具合の少ないシステム開発につながるので、意識してみましょう。