LoginSignup
0
0

React Router で可変長のパスを表現したい

Last updated at Posted at 2024-05-09

:writing_hand: はじめに

React Router について

React Router はReactアプリケーション内のルーティングを管理するためのライブラリです。(多分React のルーティングライブラリで一番有名です)

シングルページのアプリケーションにURLとコンポーネントを紐づけ従来の多ページWebサイトのような体験を実現させてくれます。

React Router で紐づけられるURLは静的なURLだけに留まらず、動的なURLとも対応させることが可能です。

静的なURLの場合

/user/123にアクセスできます。

const routes: RouteObject[] = [
    {
        path: '/user/123',
        element: <Components />
    }
]

const router = createHashRouter([...routes]);

return <RouterProvider router={router} />;
動的なURLの場合

/user/123にも/user/456アクセスできます。


const routes: RouteObject[] = [
    {
        path: '/user/:id',
        element: <Components />
    }
]

const router = createHashRouter([...routes]);

return <RouterProvider router={router} />;

:id部分はReact Router が提供するhooks を用いて参照することができます。

import { useParams } from 'react-router-dom';

const Components = () => {
    const { id } = useParams();

    console.log(id); // => /user/123の場合、123
}

可変長のパス

一部のパス部分のみが動的なURLの場合はあまり考えることがありません。

今回表現したい可変長のパスは存在しうるパターンの数だけURLのパターン増えていく為、実装方法を考える余地があります。

この記事では可変長のパスの例として、ツリー構造(ディレクトリ構造)をURLで表現してみようと思います。

:robot: 実装

TL;DR

  • *で全マッチング、useParams でツリー部分のパスを受け取りパースして自前で探索処理を行う

実装

ツリー構造のオブジェクトを用意
export const tree = [
  {
    name: "child1",
    type: "folder",
    children: [
      {
        name: "child1-1",
        type: "folder",
        children: [
          {
            name: "child1-1-1.mp3",
            type: "file",
          },
        ],
      },
      {
        name: "child1-2",
        type: "folder",
        children: [],
      },
    ],
  },
  {
    name: "child2",
    type: "folder",
    children: [
      {
        name: "child2-1",
        type: "folder",
        children: [
          {
            name: "child2-1-1",
            type: "folder",
            children: [
              {
                name: "child2-1-1-1.mp3",
                type: "file",
              },
            ],
          },
        ],
      },
    ],
  },
];
ツリー部分のパスを全マッチングするようなルートオブジェクトを用意
export const routes = [
  {
    path: "tree",
    element: <Outlet />,
    children: [
      {
        index: true,
        element: <RootPage />,
      },
      {
        path: "*",
        element: <TreePage />,
      },
    ],
  },
  {
    path: "*",
    element: <NotfoundPage />,
  },
];
export const AppRoutes = () => {
    const router = createBrowserRouter([...routes]);
    
    return <RouterProvider router={router} />;
};
ツリー構造を表示するコンポーネント作成
import { Link, useParams } from "react-router-dom";

const basePath = "/tree";

export const TreeView = ({ object, parentPath }) => {
  const { "*": matchPath = "" } = useParams();
  // 末尾が'/'の場合は削除
  const pathname = parentPath
    ? parentPath.replace(/\/$/, "")
    : `${basePath}/${matchPath}`.replace(/\/$/, "");

  return (
    <ul>
      {object.map((item, index) => {
        const path = `${pathname}/${item.name}`;

        return (
          <li key={index}>
            <Link to={path}>
              {item.name}
            </Link>
            {item.type === "folder" && item.children && (
              <TreeView object={item.children} parentPath={path} />
            )}
          </li>
        );
      })}
    </ul>
  );
};
現在のパスのオブジェクトを取得するhooks を用意
export const useFileTree = (tree, name) => {
  // treeObjectからnameに一致するオブジェクトを取得する
  const findObject = (tree, name) => {
    for (const object of tree) {
      if (object.name === name) {
        return object;
      }
      if (object.type === "folder" && object.children) {
        const found = findObject(object.children, name);
        if (found) {
          return found;
        }
      }
    }
    return undefined;
  };

  const targetTree = findObject(tree, name);
  const fileInfo = targetTree
    ? targetTree
    : { name: "Not Found", type: "file" };

  return { targetTree, fileInfo };
};
ページで表示する
import { useParams } from "react-router-dom";

import { useFileTree } from "~/hooks/useFileTree";
import { tree } from "~/utils/tree";
import { TreeView } from "~/components/Tree/TreeView";

const basePath = "/tree";

export const TreePage = () => {
  const { "*": matchPath = "" } = useParams();
  const pathname = `${basePath}/${matchPath}`.replace(/\/$/, "");
  const currentDir = matchPath.split("/").pop() || "";
  const { targetTree, fileInfo } = useFileTree(tree, currentDir);

  return (
    <div>
      <h1>名前: {fileInfo.name}</h1>
      {targetTree &&
        targetTree.type === "folder" &&
        targetTree.children.length > 0 && (
          <div>
            <TreeView object={targetTree.children} />
          </div>
        )}
        // ファイルであれば内容を表示する様な実装
    </div>
  );
};

見た目を整える

デザインを整え、パンくずリストと上へボタンを追加しました。
treeeee.gif

:straight_ruler: おわりに

React Router で動的かつ可変長なパスを表現してみました。

basePath との合成などは力技ですので、生成したパスが間違っていないか注意が必要です。

今回の実装の場合、name が同一の要素が存在すると別の内容が表示されてしまうかもしれません。より正確にマッチングさせる場合はツリー構造のオブジェクトにファイルパスの情報を持たせて比較させると探索時の計算やパス生成が容易になりそうです。

:ledger: 参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0