sagantaf

IT関連の技術記事を書くブログ。

React HooksのuseEffectとuseRef

useEffect

useEffectの基本

  • useEffectとは何か?

コンポーネントのレンダー後か、アンマウント後に処理が稼働するフック。

「レンダー後」に稼働するため、DOMを操作できる。また、アンマウントとはコンポーネントをDOMから削除して破棄することを指す。

【React】マウントとレンダリングとその違い | Milestones ReactのuseEffectとレンダリング|NOBORI Tech|note

  • 何ができるのか?

DOMの変更、APIとの通信、非同期処理などに使われる。

最新版のReact17ではいくつか破壊的変更があったらしい。

React17におけるuseEffectの破壊的変更を理解する

  • どんな使い方をするか?

以下の書き方。

useEffect(処理, 依存配列);

依存配列に格納された変数などが変更されたタイミングで処理が実行される。

下記の場合はmessageが更新されるたびに処理が実行される。

useEffect(() => {
    console.log(message);
}, [message]}

依存配列を省略すると、レンダリングの度に処理が実行される。

useEffect(() => {
    console.log(document.getElementById('texthook').innerText);
}}

なお上記は<p id='texthook'>hogehoge</p>の場合、コンソールにhogehogeと表示される。

空の配列を指定すると、最初の1回だけ処理が実行される。

useEffect(() => {
    console.log(message);
}, []}

処理内で関数を返すと、初回は何も実行されず、依存配列によって再度処理実行タイミングが来た時、もしくはアンマウントされた時に処理が実行される。 これは「クリーンアップ関数」と呼ばれる。

useEffect(() => {
    return () => {
        console.log('cleanup');
    }
}

何かのステータスを変えたり、変数定義をクリアにしたいなどに使われる。

ただし、注意点としてクリーンアップ関数は、アンマウントというタイミング上、useEffectの再レンダリングの前にも実行されてしまう。

useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  }, [props.friend.id]);

依存配列の書き忘れ防止などのためにLintツールもある(eslint-plugin-react-hooks パッケージの exhaustive-deps ルール)。

useEffectはDOM更新の後に実行したいことがある場合に利用する。たとえば、APIの実行、ログ記録など。

クラスで書く場合は、componentDidMountcomponentDidUpdateの2つのタイミングで処理を書く必要がある。もし同じ処理を実行させたい場合は2回書く必要がある。

一方で、useEffectの場合はコンポーネントのマウント直後か更新後かは区別しない。レンダリング後に実行する、という仕組みがあるだけなので、1回useEffectを書くだけで同じ挙動を実現できる。

また、クリーンアップについても、クラスの場合はcomponentWillUnmountを使って定義する必要があったが、フックの場合はreturnに処理を書くだけで実現できる。

  • DOM操作の例

レンダリング後でないとDOM上に要素が存在しない(取得しようとするとnullになる)ため、useEffectを使わないとDOM要素を取得できない。

const Message = () => {
  // ここだとレンダリング前なのでタイミングによっては
  // DOMで要素を取得できない可能性がある
  const elem = document.querySelector('#message')
  console.log(elem); // null

  // useEffectを使うとレンダリング後に実行されるので
  // 確実に取得できる
  useEffect(() => {
    const elem = document.querySelector('message');
    console.log(elem); // HTMLDivElement
    elem.innerText = 'Hello!'
  );

  return <div id='mesage'></iv>
};
  • クリーンアップ関数の例 const Timer = () => { const [time, setTime] = useState(0);

    // クリーンアップ関数を登録(return)する
    useEffect(() => {
      const timerId = setInterval(() => setTime(new Date().getTime()), 1000);
      return () => clearInterval(timerId);
    });
    
    return <div>{time}</div>
    

    };

APIからデータを取得する例

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

export default function App() {
  const [items, setItems] = useState([]);
  const [inputValue, setInputValue] = useState("react");
  const [query, setQuery] = useState(inputValue);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      )

      setItems(result.data.hits);
      setIsLoading(false);
    }

    fetchData();
  }, [query]);

  return (
    <>
      <form onSubmit={(event) => {
        event.preventDefault();
        setQuery(inputValue);
      }}>
        <input type='text' value={inputValue} onChange={(event) => setInputValue(event.target.value)} />
        <button type='submit'>検索</button>
      </form>
      {isLoading ? (
        <p>Loading</p>
      ) : (
        <ul>
          {items.map((item) => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </>
  )
}

useEffectの依存配列としてqueryを指定しているため、検索ワードが変わらないと処理は実行されない。つまり、同じ検索ワードのまま連打しても処理は1回だけ実行されることになる。

event.preventDefault();については以下を参照。

【JavaScript】event.preventDefault()が何をするのか - Qiita

useEffectを同期的に実行したい場合は

useEffectは非同期で処理されるため、ブラウザの動きをブロックしない。もし、同期させて処理させたい場合(レイアウトによって分岐させるなど)は、useLayoutEffect という別のフックが使える。機能はuseEffectと同じで、実行タイミングが同期的になり、レンダリング→useLayoutEffect→ブラウザ表示 の順に実行される。 useEffectはレンダリングの後にブラウザ表示・useEffectが同時実行されるイメージ。


useRef

useRefの基本

  • useRefとは何か

useStateと同じく値を保存できるが、値を更新してもレンダリングが発生しない違いがある。

  • 何ができるのか

DOMを参照する、もしくは前回の値を参照する時などに利用される。

  • どんな使い方をするのか

useRef(初期値)

currentプロパティで現在の値を参照できる。

const count = useRef(0);
console.log(count.current); // 0
count.current = count.current + 1
console.log(count.current); // 1

DOM参照の例

useRefで参照を作り、それをJSXに渡しておくことで、useEffectで参照できる。

const Message = () => {
  const divRef = useRef(null);

  useEffect(() => {
    // ref.currentで現在の参照の値を取得できる
    // ここではdiv要素のDOM
    divRef.current.innerText = ‘Hello!’;
  }, []);

  // refに渡しておく
  return <div ref={divRef}></div>
};

前回の値を参照する例

import React, { useState, useEffect, useRef } from "react";

export default function App() {
  const [count, setCount] = useState(10);
  const prevCountRef = useRef(0);

  useEffect(() => {
    prevCountRef.current = count;
  });

  return (
    <>
      <p>
        現在のcount: {count}, 前回のcount: {prevCountRef.current}
      </p>
      <p>前回のcountより{prevCountRef.current > count ? "小さい" : "大きい"}</p>
      <button onClick={() => setCount(Math.floor(Math.random() * 11))}>update</button>
    </>
  )
}

上記のようにすることで、useRefで定義されたオブジェクトが更新されても、再レンダーは実行されず、useEffect内の処理も稼働しない。

currentの値が更新された時のみ再レンダーが稼働し、useEffect内の処理も稼働する。

もしuseRefを使わずに let prevCountRef = 0;のようにして実装しようとすると、再レンダー時に毎回0に初期化されてしまう。

もしuseRefを使わずにuseStateを使ってしまうと、countが更新され、再レンダー→useEffect稼働となり、prevCountRefがcurrentと同じ値になった後に、画面が描画される。つまり、常に同じ値が表示されてしまい、意図とは異なる挙動になってしまう。

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon