😺

Reactの基本を学ぶ Hooks:useEffect偏

2021/07/31に公開

第一回目で一番利用頻度の高いと思われるuseStateを学びました。

続いて学ぶのはuseEffectになります。
こちらも利用頻度は高いうえに、なんとなくの理解でもある程度使えてしまうのでしっかり学んでいきたいと思います。

useEffectとは

関数コンポーネント内で副作用(effect)を実行するためのhookです。
???ですね。
まずは副作用を理解する必要がありそうです。

副作用について

副作用と聞くと大半の人が薬をイメージして悪いことが起こりそうな気がしてしますが、プログラミングにおいては副作用はとても便利で大事な概念となります。
Reactにおける副作用について、わかりやすくまとめてくださっている記事を発見しましたので、リンクを張ります。詳しくはこちらを参照してください。
https://qiita.com/Mitsuw0/items/801f783ca74b062c1ed8

簡単にまとめると、

  • DOMの書き換え
  • API通信
  • タイマー処理

あたりが私はイメージしやすかったです。
タイマー処理なんかはこの後の例題で取り上げます。

useEffectの基本形

useEffect(() => {
    effect
    return () => {
        cleanup
    }
}, [input])

vscodeで入力するとこの形でデフォルトで出力されます。焦りますね。
実際全てが必要なわけではないのでまずは切り抜いて使用していきます。

useEffectとりあえず使ってみた

おなじみのカウンターアプリで使用してみます。

import React, { useState, useEffect } from 'react';
import './App.css';

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

  console.log('useEffect実行前です');
  //試しでuseEffectを使用してみます。
  useEffect(() => {
    console.log('useEffectが実行されました');
  });
  console.log('useEffect実行後です');

  return (
    <div className='App'>
      <h1>Learn useEffect</h1>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
    </div>
  );
}

export default App;

この時注目するべきはconsole.logの順番です。
useEffectが実行されました

useEffect実行後です
の順番が逆になってますね。

この事から、useEffectを使用すると、その処理の実行をレンダリングが実行された後まで遅らせることができるという事が分かります。

この場合、+ボタンを押すと再度レンダリングが走るので、そのたびに同様の結果が得られます。

副作用を制御する 第2引数の出番

ここでもう一度useEffectの基本形をおさらいします。

useEffect(() => {
  //第1引数は実行したい副作用を操る関数
  console.log('副作用関数が実行されました!')
},[依存する変数の配列]) // 第2引数には副作用を制御するための依存データ

以上のような感じになります。

第2引数の依存データに基づき、副作用を操る為の関数を実行するかどうかを制御できます。
また、空の配列を指定することで、初回レンダリング時のみ実行させることができます。

実際の動きを見た方が速いと思うので、早速例題に移ります。

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useEffect(() => {
    console.log('useEffectが実行されました');
  }, []);

  return (
    <div className='App'>
      <h1>Learn useEffect</h1>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
      <h2>Count2: {count2}</h2>
      <button onClick={() => setCount2((prevCount) => prevCount + 1)}>+</button>
    </div>
  );
}

export default App;

空の配列をした場合は初回レンダリング時のみの実行となっているのが分かります。

次に、制御したいデータとして、countを渡します。上のカウンターの値ですね。
こちらのデータの変更を検知するたびにuseEffectが実行されることとなります。

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useEffect(() => {
    console.log('useEffectが実行されました');
  }, [count]);

  return (
    <div className='App'>
      <h1>Learn useEffect</h1>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
      <h2>Count2: {count2}</h2>
      <button onClick={() => setCount2((prevCount) => prevCount + 1)}>+</button>
    </div>
  );
}

export default App;

このようにcountの値が変更されるたびにconsole.logが走る、つまりuseEffectで制御している副作用を操る関数が実行されている事が確認できます。

もちろんでーたは一つという限定はなく、[count,count2,・・・・]のように複数指定できます。

ここで気を付けなくてはいけないのが、制御したいデータを変更する処理を同じuseEffect内で実行してしまう事です。

 useEffect(() => {
    console.log('useEffectが実行されました');
    setCount((prevCount) => prevCount + 1);
  }, [count]);

このように記述してしまうと変更を検知、実行、変更を検知、実行・・・の無限ループが発生します。これは注意してください。

少し具体的な使用例

いつものカウンターアプリではつまらないので、少し具体的な使用例を紹介します。
fetchメソッドを使用した、httpリクエストですね。

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
 //mapメソッドを使用したいので初期値を[]に
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // fetchメソッドで外部API(今回はJSONPlaceholderというテストに便利なAPI)と通信を行う
    // 今回はpostsデータを取得してくる
    fetch('https://jsonplaceholder.typicode.com/posts')
      // 帰ってきたhttp通信をjson形式に変換
      .then((res) => res.json())
      // その中のデータをsetPosts関数を使ってpostsにセットする
      // .then((data) => console.log(data));
      .then((data) => {
        setPosts(data);
      }, []);
  });

  return (
    <div className='App'>
      <h1>Learn useEffect</h1>
      <div>
        {posts.map((post) => (
          <div key={post.id}>{post.title}</div>
        ))}
      </div>
    </div>
  );
}

export default App;

このようにuseEffectを使用することで、画面の初回レンダリング時のみhttpリクエストが行われる事となり、無駄なhttpリクエストが発生することが無くなります。

おまけとして、fetch.thenで繋ぐ書き方ではなくasync/awaitを使用した書き方も練習として書いてみたので、ついでに載せておきます。動作は一緒ですが、こちらの方がおススメされているようですね。

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [posts, setPosts] = useState([]);


  async function getDataByJson() {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts');
    const json = await res.json();
    console.log(json);
    setPosts(json);
  }

  useEffect(() => getDataByJson(), []);

  return (
    <div className='App'>
      <h1>Learn useEffect</h1>
      <div>
        {posts.map((post) => (
          <div key={post.id}>{post.title}</div>
        ))}
      </div>
    </div>
  );
}

export default App;

クリーンアップ関数とは?

タイマーなどを使用した場合、コンポーネントがアンマウント(DOMから削除される事)
クリーンアップ関数をreturnすることで、前回の副作用をキャンセル(クリーンアップ)する必要が発生します。

実際に簡単なアプリを作成したので、それで動きを確認します。

  • まずはクリーンアップ関数を使用しない場合
import React, { useEffect, useState } from 'react';

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

  useEffect(() => {
    console.log('useEffectが実行されました');

    setInterval(() => {
      setCount((prevCount) => prevCount + 1);
      console.log('カウントが1アップしました');
    }, 1000);
  }, []);

  return (
    <div>
      <h1>Count: {count}</h1>
    </div>
  );
}

export default Count;


アンマウント後もconsoleにカウント処理が残っているのが確認できます。更に何やらエラーも出ていますね。メモリリークが起こってます。クリーンアップしてください。みたいなことが書いてあります。

  • クリーンアップ関数を使用した場合
    こちらは少し長くなってしまうのでコード一部省略します。
import React, { useEffect, useState } from 'react';

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

  useEffect(() => {
    console.log('useEffectが実行されました');

    const interval = setInterval(() => {
      setCount((prevCount) => prevCount + 1);
      console.log('カウントが1アップしました');
    }, 1000);

    return () => {
      clearInterval(interval);
      console.log('コンポーネントがアンマウントしました');
    };
  }, []);

  return (
    <div>
      <h3>このコンポーネントにuseEffectがかかっています</h3>
      <h1>Count: {count}</h1>
    </div>
  );
}

export default Count;

このようにクリーンアップ関数でカウント処理をキャンセルするためのclearIntervalをセットすることで、アンマウント時に先ほどのような不具合が発生することを防いでいます。

チョットしたおまけもつけてみました。

簡単にスタイリングする際はstyled-componentsが楽でいいですね。

いかがだったでしょう?
useEffectはとても利用頻度の高いhooksになるかと思います。
もっと複雑な処理であったり、様々な使い方ができると思うので、開発を通して学習していきましょう。

Discussion