Sassyブログ

好きなことで暮らしを豊かにするブログ

【React】useRouteMatchが返す戻り値をuseEffectの依存関係に含めない方がよい

react-router-dom に用意されている useRouteMatch というhooksを使って2種類のURLをマッチングして、パスパラメータを取得し数値に変換してからオブジェクトを生成してStateに保存するカスタムフックを作成しました。

このカスタムフックは特定の画面で使用するパラメータ取得用の関数で場所(URL)が変更されたことをuseEffectで検知して、URLが2種類の内どちらかにマッチしているかを検査してからパラメータを取得するようにしています。

そのuseEffectの引数にuseRouteMatchの戻り値を使用して場所の変更を検知しようとしていたが、当初無限ループに陥ってしまい原因を探っていたところuseRouteMatchが返すオブジェクトが場所が変更されなくてもuseRouteMatchを呼ぶたびに常に新しいオブジェクトを返していることが分かりました。

これは実際に react-routergithub issue でも取り上げられています。

github.com

いくつかのワークアラウンドも紹介されており、react-router v5を使っている場合はその対処を行うことで場所の変更をuseEffectで検知して処理を行うことが可能です。

github.com

そして、react-router 6.1.0にてuseRouteMatchに変わるuseMatchというhooksが返すオブジェクトをメモ化する対応がとられているので、それ以降のバージョンでは場所が変更されない限りは同一オブジェクトを返すようになっています。

github.com

まずは初期の実装例を掲載します。

※掲載コードは雑に書いてしまったので、細かいところおかしかったらごめんなさい。

export const useHogeParams = () => {
  const aMatched = useRouteMatch<HogeAPrams>('hoge/a/:aId/b/:bId/c/:cId');
  const bMatched = useRouteMatch<HogeBPrams>('hoge/c/:cId');
  const [hogeId, setHogeId] = useState<HogeId | null>(null);
  useEffect(() => { 
    if (aMatched) {
      setHogeId({
        typ: 'AUrlPattern',
        cId: parseInt(aMatched.params.cId, 10),
      });
      return;
    }

    if (bMatched) {
      setHogeId({
        typ: 'BUrlPattern',
        aId: parseInt(bMatched.params.aId, 10),
        bId: parseInt(bMatched.params.bId, 10),
        cId: parseInt(bMatched.params.cId, 10),
      });
      return;
    }

    throw new Error(`invalid url path pattern: ${location.pathname}`);
  }, [aMatched, bMatched]);

  return hogeId;
};

このコードではuseRouteMatchの引数にパスの形式を渡して、その形式にマッチしていたらMatchオブジェクトを返します。

マッチしていなかったらnullを返します。

useEffectの依存配列にはaMatchedbMatchedを設定していて、aMatchedbMatchedの変更があった場合はuseEffect内の処理を実行するようにしてました。

想定では場所が変わらなければaMatchedbMatchedインスタンスは同一のものだと思っていたのですが、そうではなかったため、無限に呼ばれ続けていました。

これを回避するためにgithub issueに載っていた方法を参考に以下のように修正しました。

export const useHogeParams = () => {
  const location = useLocation();
  const [hogeId, setHogeId] = useState<HogeId | null>(null);
  useEffect(() => {
    const aMatched = location.pathname.match('hoge/a/:aId/b/:bId/c/:cId');
    const bMatched = location.pathname.match('hoge/c/:cId');
    if (aMatched) {
      const cId = aMatched?.[1];
      if (!cId) return;
      setHogeId({
        typ: 'AUrlPattern',
        cId: parseInt(cId, 10),
      });
      return;
    }

    if (bMatched) {
      const aId = bMatched?.[1];
      const bId = bMatched?.[2];
      const cId = bMatched?.[3];
      if (!(aId && bId && cId)) return;
      setHogeId({
        typ: 'BUrlPattern',
        aId: parseInt(aId, 10),
        bId: parseInt(bId, 10),
        cId: parseInt(cId, 10),
      });
      return;
    }

    throw new Error(`invalid url path pattern: ${location.pathname}`);
  }, [location.pathname]);

  return hogeId;
};

useLocationのオブジェクトを監視する方法です。

このlocation.pathnameが変更されたときだけuseEffect内の処理が呼ばれるように修正します。

そこから指定の形式のパターンにマッチしているかどうかを判定してStateにセットするという実装にしました。

なのでuseRouteMatchが使えないというか使うケースが限られてしまっていますが、react-router 6.1.0 以降からはuseMatchを使うことで同じように書くことが可能となります。

このようなケースがあるかどうかわかりませんが、同じようなことをやりたい人向けに実装の参考になればと思います。