React hook useEffectの使い方を理解する #6

JS

useEffectについて紹介していきます。

副作用(side Effect)について触れた後、例を用いてどのように働くかを紹介していきます。

本記事の前提として、JSXコードの構造は理解されていることとしています。

またpropsの使い方やuseStateの使い方やスタイリングについては、割愛します。

1.副作用(side Effect)について

Reactの主な役割(仕事)作用/副作用(Effect/ side Effect)の役割
UIのレンダリング / user inputへの反応(処理)左記以外の何かしらの処理
・JSXの評価とレンダリング

・stateとpropsの管理

・ユーザーイベントへの反応(処理)

・stateとpropsの変化に応じたcomponentの再評価

・ブラウザstorageへのデータ保持

・httpリクエストの送信

など

component 内でpropsやstateが変化すると、JSXコードの再レンダリングが行われます。

ある特定の処理をcomponent内で実行しようとすると、propsやstateの変化に応じてJSXコードが再レンダリングされるため、ある特定処理も再実行されてしまいます。

作用・副作用(Effect/ side Effect)では、component外で実行したいある特定の処理(http リクエストなど)をuseEffect hookを使って記述することができます。

2.React hook useEffect とは

useEffect-explain

useEffect Hookでは、引数を2つ取ります。

1つ目が、副作用としてfunction(arrow関数)を設定します。

2つ目が、依存関係(dependencies)をリスト内に設定します。依存関係が複数ある場合、カンマ区切りで追加します。

componentが再レンダリングされたときではなく、依存関係設定した変数などが変化したときのみ、componentの再評価の後に副作用の処理が実行されるようになります。

3.useEffect利用前の状態

App.js

import React, { useState } from 'react';
import Login from './components/Login/Login';
import Home from './components/Home/Home';

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const loginHandler = (email, password) => {
    // this is just a dummy/ demo
    setIsLoggedIn(true);
  };

  const logoutHandler = () => {
    setIsLoggedIn(false);
  };

  return (
    <React.Fragment>
      <main>
        {!isLoggedIn && <Login onLogin={loginHandler} />}
        {isLoggedIn && <Home onLogout={logoutHandler} />}
      </main>
    </React.Fragment>
  );
}

export default App;

components/Login/Login.js

import React, { useState } from 'react';
import Card from '../UI/Card/Card';
import classes from './Login.module.css';

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 className={classes.login}>
      <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;

components/Home/Home.js

import React from 'react';
import Card from '../UI/Card/Card';
import classes from './Home.module.css';

const Home = (props) => {
  return (
    <Card className={classes.home}>
      <h1>Welcome back!</h1>
    </Card>
  );
};

export default Home;

UI/Card/Card.js

import React from 'react';
import classes from './Card.module.css';

const Card = (props) => {
  return (
    <div className={`${classes.card} ${props.className}`}>{props.children}</div>
  );
};

export default Card;

 

この状態で動作確認すると、ダミーのloginフォームにemailとpasswordを入力するとhome画面へ遷移します。

しかしブラウザをリロードすると、loginフォームに戻ってしまいます。

dummy-login-form-demo

browser storageを使って、ページをリロードしても、home画面が表示されるように一度処理を変更してみます。

 

※以下のようなApp.jsを記載すると一見よさそうにも見えます。

 const storageLoggedIn = localStorage.getItem('isLoggedIn');

 if(storageLoggedIn === '1') {
   setIsLoggedIn(true);
 }

 const loginHandler = (email, password) => {
   // this is just a dummy/ demo
   localStorage.setItem('isLoggedIn', '1');
   setIsLoggedIn(true);
 };

loginHandler内でisLoggedInに1を設定する処理を追加しているのと、local storageからisLoggedInを取得する処理と、値が1かどうか、さらにsetIsLoggedInにtrueをセットしています。

この処理は無限ループを引き起こしてしまいます。

infinite-loop

これはif内でsetIsLoggedInが実行されることで、componentの再評価が行われ、再度実行されるためです。

そのため、useEffectを利用してこの問題を解決していきます。

4.useEffect利用後の状態

App.js

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

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(()=> {
    const storageLoggedIn = localStorage.getItem('isLoggedIn');
    if(storageLoggedIn === '1') {
      setIsLoggedIn(true);
    }
  }, []);

  …

}

useEffectをimportして、useEffectの第1引数 arrow関数内に、storageからisLoggedInの値の取得と、if文を記載しています。

第2引数の依存関係は、空になっています。

この場合、アプリ起動時に1回だけ、useEffectが実行されます。

home-useEffect

login後、storageにisLoggedInがvalue=1として設定され、ページをリロードしてもhome画面が表示されるようになります。

useEffectを使うことで、無限ループを避けることができ、想定通りの挙動にすることができました。

5.useEffect(依存関係あり)

components/Login/Login.js

 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('@')
   );
 };

Login.js にて、emailChangeHandlerとpasswordChangeHandlerでバリデーションチェックをしていますが、これをuseEffectを使用して書き直してみます。

 useEffect(() => {
   setFormIsValid(
     enteredEmail.includes('@') && enteredPassword.trim().length > 6
   );
 }, [enteredEmail, enteredPassword]);

 const emailChangeHandler = (event) => {
   setEnteredEmail(event.target.value);
 };

 const passwordChangeHandler = (event) => {
   setEnteredPassword(event.target.value);
 };

useEffect内でsetFormIsValidを用いて、emailとpasswordのチェックをするように変更しました。

依存関係を空にしてしまうと、初回表示時のみのチェックとなってしまい、ユーザーからemailとpasswordに入力がされたときにチェックされなくなってしまいます。

そのため、依存関係に、enteredEmailとenteredPasswordを設定します。

これにより、emailまたはpasswordの入力に変更があるたびに、useEffect内の処理(バリデーションチェック)がされるようになります。

またsetFormIsValidのようなstate(状態)を更新する関数は変わることがないため、依存関係に入れる必要がありません。

6.クリーンアップ関数(cleanup function)

components/Login/Login.js

 useEffect(() => {
   setFormIsValid(
     enteredEmail.includes('@') && enteredPassword.trim().length > 6
   );
 }, [enteredEmail, enteredPassword]);

このuseEffectの処理自体は、問題ありません。

ただ、emailかpasswordに1文字単位で入力が変わるたびに実行されてしまいます。

この処理だけであれば、問題はないかもしれませんが、より複雑な処理が追加されたときに、1文字変わっただけで他の処理も再実行されてしまうのは、避けた方がよいケースが出てくるかと思います。

 

上記のように1文字入力単位で再実行されてしまうところを、ユーザーが入力している途中ではチェックしないようにして、またユーザーの入力が終わったまたは止まったタイミングでチェックするようにしたいと思います。

これは、setTimeout関数(browser組み込み関数)を利用することで実現することができます。

 useEffect(() => {
   setTimeout(() => {
     console.log('check form validity');
     setFormIsValid(
       enteredEmail.includes('@') && enteredPassword.trim().length > 6
     );
   }, 1000);

   return () => {
     console.log('Clean up');
   }
 }, [enteredEmail, enteredPassword]);

上記のsetTimeoutでは1秒後に、中身が実行されます。

return () => {...}

returnの部分がいわゆるクリーンアップ関数になります。

useEffectが次のuseEffectt処理を実行する前に、クリーンアップ処理が行われます。

(またDOMからcomponentがunmountされたときにもクリーンアップが実行されます)

clean-up

初回表示時、クリーンアップ関数は実行されず、文字を入力した後、呼ばれていることがわかります。

 useEffect(() => {
   const id = setTimeout(() => {
     console.log('check form validity');
     setFormIsValid(
       enteredEmail.includes('@') && enteredPassword.trim().length > 6
     );
   }, 1000);

   return () => {
     console.log('Clean up');
     clearTimeout(id);
   }
}, [enteredEmail, enteredPassword]);

clearTimeoutでクリーンアップ関数が実行されたときにsetTimeoutをキャンセルするようにします。

clean-up-clear-time

この状態で、フォームに「test」を入力した後、入力を止めると、クリーンアップが複数実行された後、1回だけsetTimeout内の処理(バリデーションチェック)が実行されていることがわかります。

初回useEffect⇒文字入力⇒cleanup⇒文字入力1秒以上なし⇒setTimeout

1秒以内に文字を入力し続けている間は、cleanup処理が働き、前のsetTimeoutがキャンセルされ、入力から1秒以上経つと、setTimeout内のバリデーションが行われるように変わりました。

7.まとめ

  • useEffect hookにて、副作用として何かしらの処理を初回起動時のみ、または変数等が変化したときだけ実行させることが可能
  • 状態更新関数をuseEffectの依存関係に追加する必要はない:Reactはこれらの関数が変更されないことを保証するため(ただし追加自体は可能)
  • 組み込みAPIやfetch()、localStorageなどの関数(ブラウザに組み込まれているためグローバルに利用可能な関数と機能)を追加する必要はない:ブラウザAPI/グローバル関数はReactとは関係ないため。 コンポーネントのレンダリングサイクルであり、変更されることもない。
  • componentの外部で定義した可能性のある変数または関数を追加する必要はない(たとえば、別のファイルで新しいヘルパー関数を作成する場合):このような関数または変数もコンポーネント関数の内部で作成されないため、 それらを変更してもコンポーネントには影響しないため。