これまで
第一回: useState
とuseRef
第二回: 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
という感じです。
コンポーネントの描画サイクルを見れば一目瞭然だと思います。
- コンポーネントの描画関数が呼び出される
-
useLayoutEffect
で設定した副作用関数が呼び出される - ブラウザのPaint処理によりコンポーネントの描画結果が画面に反映される
-
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って何だったんだって感じですよね。(あくまで筆者の見解です)