はじめに
このブログは React ベースのフレームワークである Gatsby.js で作られています。
色んな記事や書籍を参考にしながら何とか動くように実装しましたが、エラーとまで行かなくても ESLint(静的コード解析ツール)から見て「この書き方怪しいで?」みたいな警告もちらほら出ているので原因を眺めつつ解消していきたいと思います。
ちなみに Gatsby.js ではESLint がデフォルトで組み込まれており、特別な設定をしなくても利用する事が出来ます。
現在のプロジェクトではデフォルトのまま使用しています。
また、今回警告が発生しているのはブログ記事の検索・絞り込みを実装しているコンポーネントの一部のコードです。
Array.prototype.map() expects a value to be returned at the end of arrow function array-callback-return
この警告の原因はmap()
メソッドではコールバック関数から値を返す必要がありますが、その処理を行っていなかった場合に発生します。
// 初回レンダリング時のみ実行
useEffect(() => {
const posts = [];
blogData.allMarkdownRemark.edges.map(item => {
posts.push(item.node); // forEach メソッドを使用するならこの実装でも良い
});
setData(posts);
}, []);
useEffect(() => {
// warning: Array.prototype.map() expects a return value from arrow function が出ないように修正
// mapの戻り値を受けるように修正
const posts = blogData.allMarkdownRemark.edges.map(item => item.node);
setData(posts);
}, [blogData]); // ←Question? 本当ならここにも値を代入する必要があるが…
React Hook useEffect has a missing dependency: ‘xxxx’. Either include it or remove the dependency array
これはuseEffect
フックの依存配列に、フック内で使用されている全ての外部スコープの変数を含めていない場合に表示されます。
先程のコードと関係しているのですがblogData
はuseEffect
フックのスコープ外のデータだったためこのような 警告が表示されています。
useEffect
フックはこの第二引数に指定されたデータの状態を検知して、useEffect
内の関数を実行する挙動をします。空配列を指定した場合はコンポーネントがマウントされた初回のみ実行されます。
依存配列にblogData
を加えることで警告は消えますが、記事データの取得はコンポーネントがマウントしたときに一回だけ実行されていれば十分なので、空配列を指定しても ESLint の警告が出ない方向で修正します。
const SearchResult = props => {
// 全記事データ取得
const blogData = useStaticQuery(graphql`
# 記事データを取得するGraphQL
`);
// 省略
useEffect(() => {
const temp = blogData.allMarkdownRemark.edges.map(item => item.node);
setData(temp);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
同様に取得したブログ記事をフィルタリングする以下の処理でも依存配列関連の警告が出ていました。
そのコードが下記になります。
// フィルターでマッチした記事を格納する
const [result, setResult] = useState([]);
// 省略
// 記事フィルタリング機能
const blogSearch = () => {
const value = props.value.toLowerCase();
const filterData = data.filter(item => {
let tags = '';
let category = '';
let description = '';
if (item.frontmatter.tags) {
tags = item.frontmatter.tags.join(' ').toLowerCase();
}
if (item.frontmatter.description) {
description = item.frontmatter.description.toLowerCase();
}
if (item.frontmatter.cate) {
category = item.frontmatter.cate.toLowerCase();
}
const targetValue = `
${item.frontmatter.title.toLowerCase()}
${category}
${tags}
${description}
`;
return targetValue.indexOf(value) !== -1;
});
// フィルタリングした記事を格納
setResult(filterData);
};
/**************************************
* 検索処理
* テキストボックスの値が変化したときに実行される
**************************************/
useEffect(() => {
console.log('--- blogSearch useEffect ---');
blogSearch();
}, [props.value]);
検索処理のuseEffect
を以下のように修正すると動作しそうな気がしますが…、
useEffect(() => {
console.log('--- blogSearch useEffect ---');
blogSearch();
+ }, [props.value, blogSearch]);
以下の警告が表示されます。
The 'blogSearch' function makes the dependencies of useEffect Hook (at line 129) change on every render.
Move it inside the useEffect callback. Alternatively,
wrap the definition of 'blogSearch' in its own useCallback() Hook
また、ディベロッパーツールを開くと処理の実行・再レンダリングの無限ループが発生している事が見れます。
この事象が発生する原因として、
- コンポーネントがマウントされると、useEffect フックが初回実行される
blogSearch
関数が実行され、何らかの状態更新が行われる場合(ここではsetResult
が該当)コンポーネントが再レンダリングされる- コンポーネントの再レンダリングにより
blogSearch
関数が新しく再生成される useEffect
の依存配列にblogSearch
が含まれているため、その新しい参照によってuseEffect
が実行される- これが繰り返され無限ループが発生する
このような流れになります。
解決方法が先程の警告メッセージに記載されているので、それに沿って修正したいと思います。
フィルター処理を useEffect 内に移動
一番簡単な方法は、blogSearch
関数をuseEffect
内に移動する方法です。
// 記事のデータ格納
const [data, setData] = useState([]);
// 省略
useEffect(() => {
// 記事フィルター処理を useEffect 内に移動
const blogSearch = () => {
console.log('--- blogSearch ---');
// フィルタリング処理を省略
setResult(filterData);
};
blogSearch();
+ }, [props.value, data]);
これによりblogSearch
関数が毎回新しく生成されることを回避し、props.value
の値が変更される時のみ実行されるようになります。
ただし、blogSearch
関数をuseEffect
の中に定義する場合、その関数は外部からはアクセスできなくなるため、他の箇所でblogSearch
を使いたい場合はこの注意する必要があります。
useCallback を使用する方法
もう一つの方法としてblogSearch
にuseCallbackを適応させるやり方を見ていきます。
useCallback
とは関数をメモ化し、依存配列に指定された値が変更された場合にのみ関数を再生します。関数のメモ化とは関数そのものをメモリに保存して再利用する機能のことです。
ここで関数のメモ化は、関数が返す結果をキャッシュすることとは異なります。
useMemoを使用した計算結果のメモ化は関数が返す値をキャッシュするものですが、useCallback
での関数のメモ化は関数の参照(定義)自体をキャッシュします。
今回の不具合が起こっているケースでは以下のように修正します。
+ import React, { useState, useEffect, useCallback } from 'react'; // useCallback を追加
// 記事フィルター処理
+ const blogSearch = useCallback(() => { // useCallback で先程の関数をラップする
// フィルタリング処理を省略
setResult(filterData);
+ }, [props.value, data]); // 依存配列にprops.valueとdataを含める
/**************************************
* 検索処理
* テキストボックスの値が変化したときに実行される
**************************************/
useEffect(() => {
blogSearch();
}, [props.value, blogSearch]);
これで現在発生している警告を全て解消することが出来ました。
追加で何か出た場合はこの記事に追記していきます。