おすすめ自作 React hooks集2 (useRouter)


おすすめ自作 React hooks 集 2

公式ドキュメントへのリンク

useRouter

React router をロジック側で制御したいときに使用。

例えば <Link to={route1}>...</Link> でルーティングをしようとすると

  • テーブルの行をクリックしたらページ遷移させたいとき(テーブルの構造が壊れるので<tr><Link>で囲うことができない)
  • ボタンクリック時にページ遷移以外のアクションを追加したいとき(ページ遷移とカスタムボタンアクションの実行順序などを制御できない)

などで困ることが多い。

そもそも個人的にはページ遷移の制御のような高級なロジックをviewに直接書きたくはない気持ちもある(viewはonClickにコールバックを設定する程度にとどめたい)

というわけでhooksを作ります。

// use-router.ts

import { History, Location } from "history";
import { useCallback, useContext, useMemo } from "react";
import {
  match,
  RouteComponentProps,
  StaticContext,
  __RouterContext
} from "react-router";
import { RouteParam } from "../../../constants/routes";

const useRouter = () =>
  useContext<RouteComponentProps<RouteParam, StaticContext, any>>(
    __RouterContext as any
  );

const useHistory = (): History<any> => {
  const router = useRouter();
  return useMemo(() => router.history, [router.history]);
};

const useLocation = (): Location<any> => {
  const router = useRouter();
  return useMemo(() => router.location, [router.location]);
};

export const useMatch = (): match<RouteParam> => {
  const router = useRouter();
  return useMemo(() => router.match, [router.match]);
};


const usePathName = (): string => {
  const location = useLocation();
  return useMemo(() => location.pathname, [location.pathname]);  // `/users/1`
};


export const usePathNameList = (): string[] => {
  const pathname = usePathName();
  return useMemo(() => pathname.split("/").filter(x => x !== ""), [pathname]);  // `["users", "1"]`
};

/**
 * `pathname !== history.location.pathname` is added to avoid a following warning:
 * "Hash history cannot PUSH the same path; a new entry will not be added to the history stack"
 * (同じパスを2回pushしないようにしている)
 */
export const useNavigator = () => {
  const history = useHistory();
  const navigator = useCallback(
    (pathname: string) => {
      if (pathname !== history.location.pathname) {
        history.push(pathname);
      }
    },
    [history]
  );
  return navigator;
};
// routes.ts

export type RouteParam = {
  projectId: string | undefined;
  userId: string | undefined;
};

useMatch は例えば <Route path="/users/:userId" component={User} /> における :userId に入ってくるパラメータを取得するのに使います。

useMatch<T>Tany でも動きますが RouteParam のような型定義をしておくと、使うときに自動補完できて便利です。

RouteComponentProps あたりの型の指定は(手元で動かしていた範囲では十分だったものの)若干怪しいのでご指摘いただけると幸いです。

使い方

<Link to={route}>...</Link> の代替なので、BrouserRouterRoute の宣言はいつも通り必要です。

  • useNavigator
import React, { memo, useCallback, useMemo } from "react";
import { useNavigator } from "../../utils/functions/hooks/use-router";

export const UserList = memo(() => {
  const navigator = useNavigator();

  const users = [{ id: 1, name: "user1" }, { id: 2, name: "user2" }];

  const userOnClick = useCallback(
    (userId: number) => {
      navigator(`/users/${userId}`);
    },
    [navigator]
  );

  const usersWithHandler = useMemo(
    () => users.map(u => [u, () => userOnClick(u.id)] as const),
    [users, userOnClick]
  );

  return (
    <table>
      <tbody>
        {usersWithHandler.map(([user, onClick]) => (
          <tr key={user.id} onClick={onClick}>
            <td>{user.name}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
});
  • useMatch
import { memo } from "react";
import { useMatch } from "../../utils/functions/hooks/use-router";
import { UserDetail } from "./user-detail";
import { UserNotFound } from "./user-not-found";

export const user = memo(() => {
  const routerMatch = useMatch();
  const userId: string | undefined = routerMatch.params.userId;

  return userId === undefined ? (
    <UserNotFound />
  ) : (
    <UserDetail userId={userId} />
  );
});

おすすめ自作 React hooks 集は随時追加予定です。


2020/03/06 追記

この方法で <Link to={route}>...</Link> の代わりをしようとすると、anchor要素ではないせいで右クリック時のアクションなどが変わる(例えば「新しいタブで開く」とかが出ない)ことでUXが損なわれることに気づきました。
ベストな解決策が分かったらまた追記しようと思いますが、可能な限り <Link to={route}>...</Link> で遷移するようにするのが無難のようです。tableの行をanchor要素にするにはここにある方法を使えば可能のようです(まだ試せていませんが)。