はじめに
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で表現してみようと思います。
実装
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>
);
};
見た目を整える
おわりに
React Router
で動的かつ可変長なパスを表現してみました。
basePath
との合成などは力技ですので、生成したパスが間違っていないか注意が必要です。
今回の実装の場合、name
が同一の要素が存在すると別の内容が表示されてしまうかもしれません。より正確にマッチングさせる場合はツリー構造のオブジェクトにファイルパスの情報を持たせて比較させると探索時の計算やパス生成が容易になりそうです。