LoginSignup
0
1

More than 1 year has passed since last update.

これまで

第一回: useStateuseRef
第二回: useContext

に続く、Reactを掘り下げてみようシリーズです。
今回は、useEffectをメインに行こうと思います。

前提

  • React 16.8以降。なのでHook APIの使用が前提です
  • Reactの基本的な説明はしません
  • TypeScriptで書いてます

一応説明する

  • useEffect
    • コンポーネントの描画が完了した後に、副作用として呼び出されるフック
    • 副作用とは、コンポーネントの描画以外の処理を指す

使ってみる

描画時に、データ(ブログ記事など)を取得して画面に表示させます。

./src/App.tsx

import React, { useState, useEffect } from "react";
import postData from "./post-data.json";

interface IPost {
  id: string;
  title: string;
  body: string;
  date: string;
}

/** 外部APIのダミー */
const dammyAPI = async () => {
  const newPosts = postData
  return Promise.resolve(newPosts)
}

/** useEffectを実装したカスタムフック */
const usePosts = () => {
  const [posts, setPosts] = useState([] as IPost[]);
  const addPost = (newPosts: IPost[]) =>
    setPosts((allPosts) => [...newPosts, ...allPosts]);

  useEffect(() => {
    dammyAPI()
      .then(newPosts => addPost(newPosts))
      .catch(err => console.error(err))
    return () => console.log("good bye")
  }, []);  

  return posts
}

export default function App() {
  const posts = usePosts()
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

肝心な部分はコチラ

/** ココが描画後に実行される */
useEffect(() => {
  dammyAPI()
    .then(newPosts => addPost(newPosts))
    .catch(err => console.error(err))
  return () => console.log("good bye") //①
}, []);   //②
  • ① コールバック関数の戻り値を設定すると、アンマウント時に実行される
  • ② 依存配列により副作用が実行される条件を指定できる(空の場合は初回のみ実行される)
    • 変数を指定すると、その変数が更新されたタイミングで副作用が実行される

依存配列の同一性チェック

要はJavaScriptさんが何をもって同じ(もしくは変更された)と判断するか、です。
検証のため、キーボード入力のたびに再描画されるコンポーネントを用意します。

export const useAnyKeyToRender = () => {
  const [, setKey] = useState("");
  const handler = (e: KeyboardEvent) => setKey(e.key);

  useEffect(() => {
    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, []);
};

依存配列を指定しない場合
当然ですが、キーボード入力するたびにコンソールに出力(再描画)されますね。

export default function App() {
  useAnyKeyToRender();
  useEffect(() => {
    console.log("fresh render");
  });
  return <h1>Hello, React.js</h1>;
}

依存配列にプリミティブ値を指定した場合
コンソールには一回しか出力(再描画)されません。
これはword変数の内容が変わらないからですね(再代入されたとしても

export default function App() {
  useAnyKeyToRender();
  const word = "abc"
  useEffect(() => {
    console.log("fresh render");
  }, [word]);
  return <h1>Hello, React.js</h1>;
}

依存配列に配列を指定した場合
あれ、キーボード入力するたびにコンソールに出力(再描画)されますね。

  • 配列(やオブジェクト、関数)などは参照値である
  • 参照値は、データの内容ではなく同一のインスタンスかどうかで判断する
  • コンポーネントが描画される度に、新しい配列のインスタンスが生成されている
  • 都度再描画される
export default function App() {
  useAnyKeyToRender();
  const words = ["a", "b", "c"]
  useEffect(() => {
    console.log("fresh render");
  }, [words]);
  return <h1>Hello, React.js</h1>;
}

これはパフォーマンスに大きく影響しそうですね。。
ESLintで設定していれば警告してくれます。(たしかeslint-plugin-react-hooks)

回避策
同じインスタンスを参照すればいいんだから、関数外で定義すればいいんじゃね?

const words = ["a", "b", "c"]

export default function App() {
  useAnyKeyToRender();
  useEffect(() => {
    console.log("fresh render");
  }, [words]);
  return <h1>Hello, React.js</h1>;
}

正解(の一つ)です。
では、関数外で定義できない時(コンポーネントのプロパティ値を使わないといけない時など)
はどうすればいいでしょうか。。

useMemoを使う

  • 戻り値をキャッシュして、メモ化する
  • 参照するインスタンスが同一であることを保証される

ということで、コンポーネントのプロパティ値を使う場合を考えてみます。

./src/WordCount.tsx

import React, { useEffect, useMemo } from "react";
import { useAnyKeyToRender } from "./App";

const WordCount: React.FC = ({ children = "" }) => {
  useAnyKeyToRender();

  const words = useMemo(() => {
    return String(children).split(" ");
  }, [children]);

  useEffect(() => {
    console.log("fresh render");
  }, [words]);

  return (
    <>
      <p>{String(children)}</p>
      <p>
        <strong>{words.length} - words</strong>
      </p>
    </>
  );
};
export default WordCount;

./src/App.tsx

import WordCount from "./WordCount"

export default function App() {
  useAnyKeyToRender();
  return <WordCount>Hello, React.js</WordCount>;
}

はい、コンソールには一回しか出力(再描画)されません。
描画が遅いな、、と思ったら試してみるといいかもしれません。。

useCallback

似たような機能があるのでついでに。。
useMemoはメモ化された値を返すのに対して、useCallbackはメモ化された関数を返します。

import React, { useEffect, useCallback } from "react";

export const HelloWorld = () => {
  useAnyKeyToRender();

  const fn = useCallback(() => {
    console.log("hello")
    console.log("world")
  }, [])

  useEffect(() => {
    console.log("fresh render");
    fn();
  }, [fn]);
  return <h1>Hello, world</h1>;
};

export default function App() {
  return <HelloWorld />;
}

useLayoutEffect

ついでのついでですね。。呼ばれるタイミングの違うuseEffectという感じです。
コンポーネントの描画サイクルを見れば一目瞭然だと思います。

  1. コンポーネントの描画関数が呼び出される
  2. useLayoutEffectで設定した副作用関数が呼び出される
  3. ブラウザのPaint処理によりコンポーネントの描画結果が画面に反映される
  4. useEffectで設定した副作用関数が呼び出される
import React, { useEffect, useLayoutEffect } from "react";

export default function App() {
  useEffect(() => console.log("useEffect"))
  useLayoutEffect(() => console.log("useLayoutEffect"))
  return <div>ready</div>
}
/** 呼び出される順番 */
// useLayoutEffect
// useEffect

正直用途が。。描画結果が画面に反映されるまでにやること、、ウィンドウサイズを取得するとかですかね。
上手い使い方を知っている方がいらっしゃれば教えてくださいw

次回

Hookシリーズも大分きましたね。。さて次回は、、

useReducer

です。Reduxって何だったんだって感じですよね。(あくまで筆者の見解です)

参考文献

Reactハンズオンラーニング 第2版 ――Webアプリケーション開発のベストプラクティス

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1