React hook useReducerの使い方を理解する #8

JS

useReducer Hookの使い方について紹介していきます。

状態管理は、useStateを用いて行うことができますが、より複雑な状態管理を行う場合の選択肢としてuseReducerを利用することができます。

本記事では、以前紹介したuseEffectの使い方で利用したコードの一部を変更していきます。

useReducerの使い方だけであれば、本記事だけで理解できるかと思います。

またスタイリングのコードや説明は割愛しています。

1.前回の記事(useReducer利用前)

以前こちらの記事で、useEffectの使い方について紹介しました。

React hook useEffectの使い方を理解する #6
useEffectについて紹介していきます。副作用(side Effect)について触れた後、例を用いてどのように働くかを紹介していきます。useEffectを使用する前と後での挙動の違いを解説していきます。また依存関係がある場合とない場合についても触れていきます。

 

その中で、Loginフォームを以下のような形で利用していました。

Login.js(useEffect利用前)

import React, { useState } from 'react';

import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';

const Login = (props) => {
  const [enteredEmail, setEnteredEmail] = useState('');
  const [emailIsValid, setEmailIsValid] = useState();
  const [enteredPassword, setEnteredPassword] = useState('');
  const [passwordIsValid, setPasswordIsValid] = useState();
  const [formIsValid, setFormIsValid] = useState(false);

  const emailChangeHandler = (event) => {
    setEnteredEmail(event.target.value);
      setFormIsValid(
        event.target.value.includes('@') && enteredPassword.trim().length > 6
      );
  };

  const passwordChangeHandler = (event) => {
    setEnteredPassword(event.target.value);
    setFormIsValid(
      event.target.value.trim().length > 6 && enteredEmail.includes('@')
    );
  };

  const validateEmailHandler = () => {
    setEmailIsValid(enteredEmail.includes('@'));
  };

  const validatePasswordHandler = () => {
    setPasswordIsValid(enteredPassword.trim().length > 6);
  };

  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(enteredEmail, enteredPassword);
  };

  return (
    <Card>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            emailIsValid === false ? classes.invalid : ''
          }`}
        >
          <label htmlFor="email">Email</label>
          <input
            type="email"
            id="email"
            value={enteredEmail}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${
            passwordIsValid === false ? classes.invalid : ''
          }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={enteredPassword}
            onChange={passwordChangeHandler}
            onBlur={validatePasswordHandler}
          />
        </div>
        <div className={classes.actions}>
          <Button type="submit" className={classes.btn} disabled={!formIsValid}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;

Login componentでやっていることとしては、入力されたemailとpasswordに対してvalidationチェックを行っているのと、入力値の状態管理です。

enteredEmail、emailIsValidはどちらもユーザーの入力により状態が変わるため関連性はあるが、異なるstateであり、変数となっています。

 const validateEmailHandler = () => {
   setEmailIsValid(enteredEmail.includes('@'));
 };

 const validatePasswordHandler = () => {
   setPasswordIsValid(enteredPassword.trim().length > 6);
 };

別のstate(enteredEmail)を見ることでemailIsValidを上記のように配置しています。

大抵の場合は動作しますが、あるケースでは動作しない可能性があります。

例えば、enteredEmailに対する状態更新が処理されなかった場合など。

(その場合、古い状態(スナップショット)のenteredEmailに基づいて、emailIsValidが更新されうる)

このようなケースに対して、useReducerを利用することができます。

2.React useReducerとは?

状態管理には、useStateを使うことができますが、より複雑な状態管理をしたい場合があります。

状態管理や依存関係が複雑であったりする場合にuseStateを利用すると、コードが複雑になり、エラーに遭遇するケースが出てくるかもしれません。

useReducerでは、useStateの代替として、より複雑な状態管理に対して使用することができます。

useReducer-hook

他のReact Hooks同様にimport文で使えるようになります。useReducerの引数には、reducer関数と初期状態を取ります。

useReducerをあるcomponent内で使用するとなったときに、reducer関数は、componentの外側に定義できます。

上記の式の左辺側では、stateとdispatch関数になります。

stateはcomponentの再描画サイクルで使用される状態のスナップショットになります。

dispatch関数は、新しいactionをdispatchする役割があります。(状態の更新がトリガーとなり、使用される)

actionがdispatchされたときに、reducer関数が使用されます。

useReducer-flow

状態管理のフローとしては、上記のようになります。

Component内で定義したEventHandlerからDispatchを呼び出します。

Dispatchでは、action(object)をReducer関数に渡します。

Reducer関数では、action.typeに基づいて、Stateを更新します。

Stateが更新されるため、Componentは再描画されます。

3.React useReducerの使い方

Login ComponentをuseReduserを使って書き直していきます。

import React, { useState, useReducer } from 'react';

… 

const Login = (props) => {

… 

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value:'',
    isValid: null
  });

…

emailState, dispatchEmailこれらの命名は自由に決めることができます。

emailReducerを後ほど、Login Componentの外側に定義していきます。

emailStateの初期値としてobjectには、valueとisValidを持たせています。

Login Component内のemailChageHandlerとvalidateEmailHandlerを以下のように変更します。

 const emailChangeHandler = (event) => {
   dispatchEmail({ type: 'INPUT_CHANGE', value: event.target.value })
   setFormIsValid(emailState.value.includes('@') && enteredPassword.trim().length > 6);
 };

 …

 const validateEmailHandler = () => {
   dispatchEmail({ type: 'INPUT_BLUR' })
 };

EventHandler内でdispatchを呼び出して、action(object)を渡しています。

 …

const emailReducer = (state, action) => {
  if(action.type === 'INPUT_CHANGE') {
    return { value: action.value, isValid: action.value.includes('@') }
  }

  if(action.type === 'INPUT_BLUR') {
    return { value: state.value, isValid: state.value.includes('@') }
  }

  return { value: '', isValid: false }
}

const Login = (props) => {
…

emailReducer関数をLogin Componentの外側に定義しました。

action.typeがINPUT_CHANGEのとき、新しいstateを返しています。

action.typeがINPUT_BLURのとき(マウスフォーカスがinputから外れたとき)、最後のstate(スナップショット)を返しています。

その他submitHandlerとJSXコード内の記載

const submitHandler = (event) => {
  event.preventDefault();
  props.onLogin(emailState.value, enteredPassword);
};

JSXコード内

 return (
   <Card>
     <form onSubmit={submitHandler}>
      …
       <label htmlFor="email">Email</label>
       <input
         type="text"
         id="email"
         value={emailState.value}
         onChange={emailChangeHandler}
         onBlur={validateEmailHandler}
       />
       …
     </form>
   </Card>
 );

passwordの方も同様に変更することができます。

4. useReducer 利用後の処理

passwordにもuseReducerを使用した全体のコードは以下のようになります。

Login.js

import React, { useState, useReducer } from 'react';

import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';

const emailReducer = (state, action) => {
  if(action.type === 'INPUT_CHANGE') {
    return { value: action.value, isValid: action.value.includes('@') }
  }

  if(action.type === 'INPUT_BLUR') {
    return { value: state.value, isValid: state.value.includes('@') }
  }
  return { value: '', isValid: false }
}

const passwordReducer = (state, action) => {
  if(action.type === 'INPUT_CHANGE') {
    return { value: action.value, isValid: action.value.trim().length > 6 }
  }

  if(action.type === 'INPUT_BLUR') {
    return { value: state.value, isValid: state.value.trim().length > 6 }
  }
  return { value: '', isValid: false }
}

const Login = (props) => {
  const [formIsValid, setFormIsValid] = useState(false);

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: '',
    isValid: null
  });

  const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
    value: '',
    isValid: null
  });

  const emailChangeHandler = (event) => {
    dispatchEmail({ type: 'INPUT_CHANGE', value: event.target.value })
    setFormIsValid(emailState.value.includes('@') && passwordState.value.trim().length > 6);
  };

  const passwordChangeHandler = (event) => {
    dispatchPassword({ type: 'INPUT_CHANGE', value: event.target.value })
    setFormIsValid(passwordState.value.trim().length > 6 && emailState.value.includes('@'));
  };

  const validateEmailHandler = () => {
    dispatchEmail({ type: 'INPUT_BLUR' })
  };

  const validatePasswordHandler = () => {
    dispatchPassword({ type: 'INPUT_BLUR' })
  };

  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(emailState.value, passwordState.value);
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            emailState.isValid === false ? classes.invalid : ''
          }`}
        >
          <label htmlFor="email">Email</label>
          <input
            type="text"
            id="email"
            value={emailState.value}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${
            passwordState.isValid === false ? classes.invalid : ''
          }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={passwordState.value}
            onChange={passwordChangeHandler}
            onBlur={validatePasswordHandler}
          />
        </div>
        <div className={classes.actions}>
          <Button type="submit" className={classes.btn} disabled={!formIsValid}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;

※本記事では、スタイリングに関する解説は省略しています。

動作自体は、前回と同様の挙動となります。

5.useEffectを使ったvalidationチェック

以下の処理でemailとpasswordのvalidationチェックを行っていますが、useEffectを使用して、チェックすることもできます。

 const emailChangeHandler = (event) => {
   dispatchEmail({ type: 'INPUT_CHANGE', value: event.target.value })
   setFormIsValid(emailState.value.includes('@') && passwordState.value.trim().length > 6);
 };

 const passwordChangeHandler = (event) => {
   dispatchPassword({ type: 'INPUT_CHANGE', value: event.target.value })
   setFormIsValid(passwordState.value.trim().length > 6 && emailState.value.includes('@'));
 };

useEffectでは、validationだけしたい、emailまたはpasswordの値が変更されるたびに実行されるのは避けたいとします。

その場合、以下のように書き換えることができます。

import React, { useState, useEffect, useReducer } from 'react';
…
const Login = (props) => {
… 
  const { isValid: emailIsValid } = emailState;
  const { isValid: passwordIsValid } = passwordState;
  useEffect(() => {
    const id = setTimeout(() => {
      console.log('check form validity');
      setFormIsValid(
        emailIsValid && passwordIsValid
      );
    }, 1000);
    return () => {
      console.log('Clean up');
      clearTimeout(id);
    }
  }, [emailIsValid, passwordIsValid]);

  const emailChangeHandler = (event) => {
    dispatchEmail({ type: 'INPUT_CHANGE', value: event.target.value })
    // setFormIsValid(emailState.value.includes('@') && passwordState.value.trim().length > 6);
  };

  const passwordChangeHandler = (event) => {
    dispatchPassword({ type: 'INPUT_CHANGE', value: event.target.value })
    // setFormIsValid(passwordState.value.trim().length > 6 && emailState.value.includes('@'));
  };
…

分割代入(Destructuring)を利用することで、emailStateからisValidを取り出すことができます。(passwordも同様)

また別名としてemailIsValidを割り当てています。

useEffect内では、emailIsValidまたはpasswordIsValidのみが変わったときのみ、再実行されるようになりました。

6.useReducer VS useState

useStateを使用すると煩雑になるようなとき、またはバグや意図しない挙動になり得るとき、useReducerが必要になることがわかりました。

useStateuseReducer
主に状態管理のツールより複雑な状態管理のツール
独立した状態/データに対して有効関連のある状態/データに対して有効
状態の更新が簡単で、更新が制限されている場合に有効より複雑な状態の更新に対して有効(使用する選択肢になる)

どちらを使用するかはケースバイケースなため、アプリのスケールに合わせて適宜使用する必要はあるかと思います。