useEffectの第2引数には何を指定すればよいのか

https://ja.reactjs.org/docs/hooks-reference.html#useeffect

useEffectの第2引数として配列を渡すことで配列に指定した値それぞれに変更があった場合のみuseEffect内の関数が実行されます。

useEffect内でなにか関数を実行する際、第2引数として関数も渡すことがあると思いますが、これが正しいのかどうか分からず調べてみました。

例えばAPIから取得したデータをstateにセットするみたいなコードがあったとします(コードは適当です)

const Component = () => {
  const [count, setCount] = useState(0);
  const { response } = useAPI();
  
  useEffect(() => {
    setCount(response.length);
  }, [response, setCount]);   // ここでsetCountって必要?
}

このset関数はuseEffectの第2引数に含める必要があるのでしょうか?結論から言うと↑のコードでは setCount はセットする必要はありません。

useStateのドキュメントに以下のような記述があり、再レンダリングされた場合もset関数は同一性が担保されておりuseEffectの依存リスト(第2引数)に含めても値が変わることがないので入れる必要がありません。

同様にuseReducerも同じような記述がありdispatchも入れる必要がありません。

通常の関数は再レンダリング時に同一性がない

以下のように書くと、関数で実行する内容は変わらないにもかかわらず再レンダリングされるたびにuseEffectが実行されします。

const Component = () => {
  const [count, setCount] = useState(0);

  const hello = () => {
    console.log('hello!');
  }
  
  useEffect(() => {
    hello();
  }, [hello]);
}

これは再レンダリングのたびに関数が再生成されるため、変更があったかどうかを判断するObject.is()はfalseとなりuseEffectが実行されます。↓のイメージです。

Object.is(() => null, () => null);  // => false

上記の場合どうするのがいいのかというと、A Complete Guide to useEffectによれば、propsやstateに依存しない関数の場合はコンポーネントの外で関数を定義し、依存する場合はuseEffect内で関数を定義するのがよいとのこと。

// コンポーネントの外で定義
const hello = () => {
  console.log('hello!');
}

const Component = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    hello();
  }, []);
}
const Component = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // useEffect内で定義
    const hello = () => {
      console.log(data);
    }

    hello();
  }, [data]);
}

useCallbackを使って関数自体をメモ化して再レンダリングされた場合でも同一のものを返すということもできますが、公式ドキュメントには最終手段と書いているので、上記2つで書くことができない場合に使うとよさそうです。

const Component = () => {
  const [count, setCount] = useState(0);

  const hello = useCallback() => {
    console.log(data);
  }, [])

  useEffect(() => {
    hello();
  }, [hello]);
}

ESLintのeslint-plugin-react-hooksプラグイン

ESLintのeslint-plugin-react-hooksプラグインを利用しreact-hooks/exhaustive-depsのルールをONにすることで、useEffect, useCallbackの第2引数を自動でチェックすることができます。

上記のuseStateのset関数やuseReducerのdispatch関数、useRefのrefはあらかじめ除外されており、チェックされないので含める必要がありません。また、通常の関数についても再レンダリングのたびにuseEffectが実行される場合はlintで教えてくれます。

カスタムフックに処理を出している場合に注意が必要

カスタムフック内で関数を定義し、それをコンポーネント側で利用するときはこのようなコードになるかと思います。

const useHello = () => {
  const hello = (count) => {
    console.log('hello! count', count);
  }
  return { hello };
}

const Component = () => {
  const [count, setCount] = useState(0);
  const { hello } = useHello();

  useEffect(() => {
    hello(count);
  }, [count, hello]);
}

このコンポーネントが再レンダリングされたとき、countが変更されているいないに関わらずuseEffectが実行されます。この場合はESLintのreact-hooks/exhaustive-depsでも検知することはできないため、カスタムフックの実装時に関数は同一性が担保されるように実装する必要があります。

関数の実行結果を第2引数に指定してもよいか

あまり書かないでしょうがこんなコードです。

const neededCountUp = () => {
  return true;
};

const Component = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(prev => prev + 1);
  }, [neededCountUp()]);
}

これも結論から言うと動作はしますが、当然再レンダリング時に前回の情報とのObject.is()で変更があったかを判断するため、↑のコードでは関数の返り値として常にtrueが返ってきており2回目以降のレンダリング時にはuseEffectは実行されしません。再レンダリング時に関数が毎回実行され返り値からuseEffectを実行させるか判断するという挙動となりあまり効率は良くないコードかもしれません。

ちなみに↑のようなコードではESLintoのreact-hooks/exhaustive-depsではチェックにかかり、関数実行の返り値の別の変数にいれましょうというメッセージが出ます。

JestのmoduleNameMapperで画像のfile mockができなくてハマった

const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { compilerOptions } = require('./tsconfig.json');

module.exports = {
  moduleNameMapper: pathsToModuleNameMapper(
    compilerOptions.paths,
    { prefix: '<rootDir>/' }
  )
}

tsconfig.jsonのcompilerOptionsにpaths設定をしているので、pathsToModuleNameMapperを使ってみたいな感じで書いていた。

svgファイルをimportするコードがあったので https://jestjs.io/ja/docs/webpack を参考にfileMockさせようと思いこんな感じで書いた。

const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { compilerOptions } = require('./tsconfig.json');

module.exports = {
  moduleNameMapper: pathsToModuleNameMapper(
    {
      "\\.(jpg|jpeg|png|gif|svg)$": ["./src/tests/mocks/fileMock.js"],
      ...compilerOptions.paths,
    },
    { prefix: '<rootDir>/' }
  )
}
module.exports = 'test-file-stub';

するといっこうにfileMockが効かない。。。
2時間くらい格闘したけど、結論pathsToModuleNameMapperの中にいれていたのが間違いで、外に設定すれば動いた。

const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { compilerOptions } = require('./tsconfig.json');

module.exports = {
  moduleNameMapper: {
    "\\.(jpg|jpeg|png|gif|svg)$": ["<rootDir>/src/tests/mocks/fileMock.js"],
    ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
  },
}

pathsToModuleNameMapperのソースコードを見てみるといろんなエスケープ処理が入っていて、tsconfig.jsonのcompilerOptions.paths用に作られていた。(そりゃそうだ)

pathsToModuleNameMapperにいれると↓みたいになっててそりゃ一致しないよねってことでした。

{
  '^\\\\\\.\\(jpg\\|jpeg\\|png\\|gif\\|svg\\)\\$$': '<rootDir>/./src/tests/mocks/fileMock.js',
}

プロジェクトを管理するときに大事にしていること

特に出典とかはありませんが僕の経験から大事にしていることなどを書きました。
社内向けのドキュメントに書いていましたが、外にも出せるかなと思い少しだけ修正しつつ記事にしました。

全体像をきちんと理解する

  • 全体像を理解するとはプロジェクトの5W1Hを理解するということ
    • Why: なぜそれが必要なのか、誰が喜ぶのか
    • What: 何をつくるのか、何が必要なのか
    • When: いつまでにつくりたいのか
    • Who: 誰が関わり、誰がつくるのか
    • Where: どこにつくるのか、どこでつくるのか
    • How: どういう方法でつくるのか
  • 全体像を把握しないと詳細に落とし込むことができない
    • 理解してない状態で詳細に落とし込むと認識違いの手戻りや仕様変更の確率があがる
    • 疑問点は知ってる人に聞きまくって理解する
  • 把握した上でだいたいのスケジュール感でどこまでのものが提供できるのか想像する

なぜ自分がこのプロジェクトに参加しているのかを考える

  • どういう役割を求められているのか
  • 自分の何がプロジェクトに活かせるのか
  • 自分がこのプロジェクトを完了させたときに何が得られているのか
  • 何をモチベーションにしていくか
  • 「自分のやりたいこと」と「リリースまでのスケジュール」の交わる点がプロジェクトのゴール
    • 「自分のやりたいこと」だけやってもスケジュールが伸びるだけ
    • 「リリースまでのスケジュール」だけを優先してもモチベーションが上がらない
    • ちょうどいい感じのポイントを探す

管理する = 偉いではない

  • プロジェクト管理することは偉いことではない
    • 単純にそういう立場であるだけ
    • 求められているものがプロジェクトを管理することであるだけ
  • 人を管理するではない、タスクを管理する
  • 上からの操り人形みたいなイメージではなくむしろ下から押し上げていくイメージ

不確実性コーンを意識する

こういうやつ

  • どんなに仕様を理解して全体を把握していても考慮漏れや仕様変更が必ずどこかである
    • 今まで完璧にタスク分解できて考慮漏れがなかったことは一度もない
  • 人間はリリースが先であるほど余裕をカマすのでスケジュールが巻くことはまずない
    • 不確実性コーンで言えば下半分はほぼない
    • もし余裕があれば(だいたいないけど)
      1. テストの拡充や自動化で品質を上げる
      2. リファクタリングなどで技術的負債を少なくする

定期的にスケジュールを見直す

  • スケジュールは基本遅れるものとして考える
    • スケジュールをわざと遅らせてやろうと思っている人間はいない
    • 遅れることは仕方がないのでそこから未来のことを考える
  • 遅れが発生した時に都度スケジュールを見直す
  • 「スケジュールを見直す」とは
    • プロジェクトの開始から見て見積り精度がどうだったかを見直す
    • タスクの漏れがないかどうか見直す
    • タスクの優先度を見直す
    • メンバーのアサインを見直す
    • スケジュールを引き直す
  • 見直しをしないといつまでも不確実性が小さくならない
  • 見直したスケジュールは共有する

ツールはなんでもいい

  • 定期的に見直すことさえすれば正直なんでもいい
  • ツールにこだわりすぎるとツール自体に時間をかけてしまったり逆に制限がかかったりする
  • 要は第三者に遅れているのか、順調なのか、間に合うのかが説明できればいい
  • あとは自分が見直しやすい感じで

決して無理をしない

  • 今いるメンバーで可能な範囲でつくる、スケジューリングする
    • リリースまでに間に合うか
    • レビューが可能か
    • リリース後のメンテナンスが可能か
  • 背伸びしすぎない
    • 「今流行りらいしこれ使おうぜ」
      • 誰もメンテナンスできなくて最悪みたいなことが起こる
    • 今いるメンバーがちょっと学びがあるくらいがちょうどいい
  • 人間がこなせるタスク量は限界がある
    • 無理をすると絶対にどこかがほころぶ
    • 身体的、精神的な疲労は慢性的に蓄積する

情熱みたいなものも時には必要

  • 人間は感情で動く生き物
  • 情熱や熱意は人に影響を与える
  • 「あなたがそう言うなら」といかに人に思ってもらえるか
  • スキルも大事だけど最終的には人間性がものをいう