React入門

連載目次 : React入門
前回の記事 : useState / React Hooks
次回の記事 : useMemoとuseCallback / React Hooks

前回のuseStateの解説につづきReat Hooksで最も重要な機能であるuseEffectについて解説を行います。

useEffectはClassコンポーネントに指定できるライフサイクルイベントの代替手法として紹介されることが多いのですが、その理解ですと上手に使いこなすことは難しいです。

useEffectを正しく取り扱うにはfunctionコンポーネントの再描画(リレンダリング)の仕組みを知る必要があるのでまずは再描画の解説から行います。

functionコンポーネントの再描画

functionコンポーネントはJSXとして読み込まれた際に最初の描画が行われます。

そして、アプリケーション内で以下の変化が発生したタイミングで再描画されます。

  • 親コンポーネントが再描画された時
  • 親コンポーネントから引き渡されているpropsが変化した時
  • コンポーネント内でuseStateで定義している変数が変化した時
  • カスタムフックより受け取っている変数が変化した時

再描画の際にコンポーネントは初回と同じ処理が実行されるのではなく一部の処理は前回のコンポーネントの状態を引き継ぎます。

たとえばuseStateで値が変化されている状態で再描画が発生すると、useStateで取得される値は初期値ではなく、最後にコンポーネントが保有していた値になります。

以下のコードではボタンをクリックした際にuseStateで定義している値が変化するので、Fooコンポーネントが再描画されFooコンポーネント内で定義している処理が再実行されます。そのため console.log('再描画',count)が毎回実行されるのですが、参照している countは初期値ではなく変更後の値が参照できるようになっています。

const Foo = () => {
  const [count, changeCount] = useState(0)
  console.log('再描画',count)
  return (
    <>
      <p>カウント:{count}</p>
      <input type="button" value="10" onClick={() => changeCount(10)} />
    </>
  )
}

useEffectを利用すると初回描画もしくは再描画時に指定した変数に変化があった場合のみにコールバック関数に指定した処理を実行することが可能になります。

useEffectの基本

useEffectuseStateと同様にReactのimport時にuseEffectを読み込むことで利用できるようになります。

import React, { useEffect } from 'react';

そして、Function Componentのトップレベルの位置で以下の宣言を行い実行したい処理を定義します。第2引数には依存変数を配列で指定しておくことで依存変数に変更があった場合のみに指定した処理が実行されるようになります。

useEffect(() => {
  // 実行したい処理を記述
},[依存変数を配列で記述])

第2引数は省略可能で、省略した場合にはfunctionコンポーネントの再描画時に毎回実行されるようになりますが、無限ループの温床になりやすくほとんどのケースでは第2引数の指定が必要となります。

それでは、いくつかの実例をもとにuseEffectの利用方法を見ていきますしょう。

useEffectを利用した通信処理

コンポーネントが読み込まれた場合に通信を行いたい場合などはuseEffectを利用します。

以下のサンプルではAjaxで取得してきたデータをuseStateで定義したarticleというState に格納して 描画するサンプルです。

const Foo = () => {
  const [articles,changeArticle] = useState([])
  
  useEffect(()=>{
    console.log("fetch")
    const url = `https://xxx/lists?page=1`
    fetch(url)
      .then( res => res.json() )
      .then( res => {
        changeArticle(res);
      })
  })
  
  return (
    <>
      <ul>
        {articles.map(article=>(
          <li>{article.title}</li>
        ))}
      </ul>
    </>
  )
}

こちらの処理を実行するとコンソール上に fetchが何度も表示されてしまいます。

Ajax通信後にchangeArticleを実行してStateを更新した際にコンポーネントの再描画が走りuseEffect内の処理が再実行されてしまうためです。

無限ループにならないようにuseEffectの第2引数に依存変数を配列で指定しますが初回のみ実行したい場合は空で指定を行います。

  useEffect(()=>{
    console.log("fetch")
    const url = `https://xxx/lists?page=1`
    fetch(url)
      .then( res => res.json() )
      .then( res => {
        changeArticle(res);
      })
  },[]) // ここを変更

これでuseEffectで処理した内容は初回描画時のみに実行されchangeArticleが実行された後に再実行されることはありません。

ページングの追加

上記のコードにページング処理を付けてみましょう。

const Foo = () => {
  const [page,changePage] = useState(1)
  const [articles,changeArticle] = useState([])
  
  useEffect(()=>{
    const url = `https://xxx/lists?page=${page}`
    fetch(url)
      .then( res => res.json() )
      .then( res => {
        changeArticle(res);
      })
  },[])
  
  return (
    <>
      <p>{page}ページ</p>
      <input type="button" value="次" onClick={()=>changePage(prevPage=>prevPage+1)} />
      <ul>
        {articles.map(article=>(
          <li>{article.title.rendered}</li>
        ))}
      </ul>
    </>
  )
}

あらたにpageというステートを追加して表示するページ数を定義しています。

ページ内には次へボタンを設置してクリックするたびにページが増加するように定義しています。

ボタンをクリックしていくとp要素内にページ数は切り替わりますが、表示されているリストは切り替わりません。

これはuseEffectの依存変数に何も指定していないため、コンポーネント初回描画時にしか実行されずpageが変わった場合に再実行されないためです。

ureEffectの第2引数を以下のように変更するだけで修正することができます。

  useEffect(()=>{
    const url = `https://xxx/lists?page=${page}`
    fetch(url)
      .then( res => res.json() )
      .then( res => {
        changeArticle(res);
      })
  },[page]) // ここを変更

これによりステートpageが変更された場合もuseEffectで定義した処理が実行されてあたらしい情報がfetchされて表示されるようになります。

このように useEffectを利用する場合には第2引数に適切な依存変数を指定しなくてはいけないので注意が必要です。

useEffectの返り値

useEffectで指定したコールバック関数には返り値で関数を指定することができ、指定した関数は以下のタイミングで実行されます。

  • ClassコンポーネントのライフサイクルメソッドcomponentWillUnmountのようにコンポーネントが破棄されるタイミング
  • useEffectの依存変数に変更が起こり、新たなuseEffectが実行される前

これはタイマーやイベントなどの再設定によく利用されます。

例えば次のように setIntervalを利用して1秒ごとにコンソールに現在のカント数を出力しようとした場合、changeCountが実行されるたびに新たにタイマーが追加されていく形になります。

const Foo = () => {
  const [count, changeCount] = useState(0)
  
  useEffect(()=>{
    setInterval(()=>{
      console.log(count)
    },1000)
  },[count])

  return (
    <>
      <p>カウント:{count}</p>
      <input type="button" value="10" onClick={() => changeCount(10)} />
    </>
  )
}

このような処理を行いたい場合、useEffectで新たなタイマーが発行される前に一度clearIntervalを実行して古いタイマー処理を破棄することが可能になります。

  useEffect(()=>{
    const timer = setInterval(()=>{
      console.log(count)
    },1000)
    return () => {
      clearInterval(timer)
    }
  },[count])

windowイベントなどに処理を追加したい場合も同様です。useEffectで購読処理を設定するのと同時に、返り値で購読破棄の処理を指定することでイベントが重複して設定され続けることを防ぐことができます。

  useEffect(()=>{
    const handleScroll = () =>{
      //スクロール時に実行したい処理
    }
    window.addEventListener(handleScroll)
    return () => {
      window.removeEventListener(handleScroll)
    }
  },[])

このようにuseEffectを利用することで任意のタイミングで処理の実行や制御が可能になります。

useEffectを適切に利用しないとReactの再描画が頻繁に起こりパフォーマンスを損ねる原因になってしまうので注意が必要です。

次回はコンポーネントの再描画時に直前の結果を呼び出す、useMemo / useCallback について解説を行います。

連載目次 : React入門
前回の記事 : useState / React Hooks
次回の記事 : useMemoとuseCallback / React Hooks