React(Hooks)によるBreadcrumb(パンくずリスト)実装
勉強のためにあえてコンポーネントやライブラリを使わずに独自実装
あくまで勉強用ですので...色々な実装方法もありますので
分割もしていないので見通しの悪いコードですが・・・
完成イメージ
こんな感じでパスに応じて画面遷移(しているように)パンくずでナビゲーションします。
前提
- デザイン
Bulmaを使ってます。 - React
$ create-react-app sample-app --template typescript
仕様
- ページ定義は配列で設定(パンくずテキスト、アイコン、パス、表示コンポーネントを定義)
- ルーティングは上記配列から吐き出す
- ナビゲーション部分にパンくずコンポーネントを配置
- パンくずコンポーネントはカスタムフックで表示内容を取得
- パンくず状態はコンテキスト管理
パンくず設定カスタムフックロジック(useBreadcrumb())
1.Routerのフック(useLocation())を使って、現在のパスを取得
const location = useLocation();
2.パスからルート定義配列を引き当て
// find current item
const currentItem = routes.find((route) => location.pathname === route.path);
if (!currentItem) {
Error(`can't find currentItem look route item define path=>${currentPath}`);
}
3.パスが「/」の場合、何もしない(処理終了)
// route path is exit
if (currentPath === routes[0].path) {
return [[currentItem!], currentItem!];
}
4.既存のパンくずアイテムをPOP
// pop items
let popedItems: RouterItem[] = []; // // pop item
let currentItemIndex = breadcrumb.items.findIndex(
(item) => currentItem?.path === item.path
);
if (currentItemIndex !== -1) {
popedItems = breadcrumb.items
.map((item, index, items) => {
if (currentItem?.path !== item.path && currentItemIndex > index) {
return item;
}
return undefined;
})
.filter(Boolean) as RouterItem[];
} else {
if (currentPath.split("/").length > 2) {
popedItems = breadcrumb.items;
} else {
popedItems = [routes[0]];
}
}
5.現在のアイテムを追加
//push current item
const pushedItems = [...popedItems, currentItem] as RouterItem[];
// set context value
breadcrumb.items = pushedItems;
breadcrumb.current = currentItem!;
アイテムをPOPする部分が強引すぎる気が・・・
関連する画面遷移はパスとして「/page1/page2」となっている必要がある・・・
コード
分割していないので長いですが・・・
src/App.tsx
import React, { useState } from "react";
import {
Link,
BrowserRouter,
Route,
Switch,
useLocation,
} from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faEdit,
faHome,
faList,
faPlusCircle,
} from "@fortawesome/free-solid-svg-icons";
type Breadcrumb = {
items: RouterItem[];
current: RouterItem;
update: (items: RouterItem[], current: RouterItem) => void;
};
type RouterItem = {
path: string;
exact?: boolean;
icon: () => JSX.Element;
text: string;
component: () => JSX.Element;
};
const routes: RouterItem[] = [
{
path: "/",
exact: true,
icon: () => <FontAwesomeIcon icon={faHome} size="sm" />,
text: "Home",
component: () => <Home title="Home" />,
},
{
path: "/page1",
exact: true,
icon: () => <FontAwesomeIcon icon={faPlusCircle} size="sm" />,
text: "Page1",
component: () => <Page1 title="Page1" />,
},
{
path: "/page1/page2",
exact: true,
icon: () => <FontAwesomeIcon icon={faEdit} size="sm" />,
text: "Page2",
component: () => <Page2 title="Page2" />,
},
{
path: "/page3",
exact: true,
icon: () => <FontAwesomeIcon icon={faList} size="sm" />,
text: "Page3",
component: () => <Page3 title="Page3" />,
},
];
const BreadcrumbContext = React.createContext<Breadcrumb>({
items: [routes[0]],
current: routes[0],
update: () => {},
});
const useBreadcrumb = (): [RouterItem[], RouterItem] => {
console.log("useBreadcrumb() ---------------------");
const breadcrumb = React.useContext(BreadcrumbContext);
const location = useLocation();
const currentPath = location.pathname;
// find current item
const currentItem = routes.find((route) => location.pathname === route.path);
if (!currentItem) {
Error(`can't find currentItem look route item define path=>${currentPath}`);
}
// route path is exit
if (currentPath === routes[0].path) {
return [[currentItem!], currentItem!];
}
// pop items
let popedItems: RouterItem[] = []; // // pop item
let currentItemIndex = breadcrumb.items.findIndex(
(item) => currentItem?.path === item.path
);
if (currentItemIndex !== -1) {
popedItems = breadcrumb.items
.map((item, index, items) => {
if (currentItem?.path !== item.path && currentItemIndex > index) {
return item;
}
return undefined;
})
.filter(Boolean) as RouterItem[];
} else {
if (currentPath.split("/").length > 2) {
popedItems = breadcrumb.items;
} else {
popedItems = [routes[0]];
}
}
//push current item
const pushedItems = [...popedItems, currentItem] as RouterItem[];
// set context value
breadcrumb.items = pushedItems;
breadcrumb.current = currentItem!;
console.log(breadcrumb.current);
console.log(breadcrumb.items);
console.log("useBreadcrumb() ---------------------");
return [pushedItems, currentItem!];
};
const BreadcrumbComponent = () => {
console.log("BreadcrumbComponent() -----------------------------");
const [breadcrumbItems] = useBreadcrumb();
return (
<nav className="breadcrumb">
<ul>
{breadcrumbItems.map((item, index, items) => {
return items.length !== index ? (
<li key={index}>
<Link to={item.path}>
{item.icon()}
{item.text}
</Link>
</li>
) : (
<li key={index}>
<Link to={item.path} className="is-active">
{item.icon()}
{item.text}
</Link>
</li>
);
})}
</ul>
</nav>
);
};
const Nav = () => {
return (
<>
<nav
className="stroke navbar is-black"
role="navigation"
aria-label="main navigation"
>
<div className="navbar-brand">
<div className="navbar-item">
<h1 className="title has-text-white">Sample APP</h1>
</div>
<a
role="button"
className="navbar-burger"
aria-label="menu"
aria-expanded="false"
href="###"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div className="navbar-menu">
<ul>
<li>
<Link to="/">
<FontAwesomeIcon icon={faHome} size="lg" className="fa fa-fw" />
Home
</Link>
</li>
<li>
<Link to="/page1">
<FontAwesomeIcon
icon={faPlusCircle}
size="lg"
className="fa fa-fw"
/>
Page1
</Link>
</li>
<li>
<Link to="/page3">
<FontAwesomeIcon icon={faList} size="lg" className="fa fa-fw" />
Page3
</Link>
</li>
</ul>
</div>
</nav>
<BreadcrumbComponent />
</>
);
};
const Home: React.FC<{ title: string }> = (props) => {
console.log("Home ----------------------------------------------");
return (
<>
<div className="container">
<h1 className="title">{props.title}</h1>
<h2 className="subtitle">Breadcrumb Example</h2>
<div className="field">
<div className="control">
<Link to="/page1" className="subtitle">
<FontAwesomeIcon icon={faPlusCircle} size="2x" />
Page1
</Link>
</div>
<div className="field">
<div className="control">
<Link to="/page3" className="subtitle">
<FontAwesomeIcon icon={faList} size="2x" />
Page3
</Link>
</div>
</div>
</div>
</div>
</>
);
};
const Page1: React.FC<{ title: string }> = (props) => {
console.log("Page1 ----------------------------------------------");
return (
<>
<div className="container">
<h1 className="title">{props.title}</h1>
<div className="field">
<div className="control">
<Link to="/" className="subtitle">
<FontAwesomeIcon icon={faHome} size="2x" />
Home
</Link>
</div>
</div>
<div className="field">
<div className="control">
<Link to="/page1/page2" className="subtitle">
<FontAwesomeIcon icon={faEdit} size="2x" />
Page2
</Link>
</div>
</div>
</div>
</>
);
};
const Page2: React.FC<{ title: string }> = (props) => {
console.log("Page2 ----------------------------------------------");
return (
<>
<div className="container">
<h1 className="title">{props.title}</h1>
<div className="field">
<div className="control">
<Link to="/" className="subtitle">
<FontAwesomeIcon icon={faHome} size="2x" />
Home
</Link>
</div>
</div>
<div className="field">
<div className="control">
<Link to="/page1" className="subtitle">
<FontAwesomeIcon icon={faPlusCircle} size="2x" />
Page1
</Link>
</div>
</div>
</div>
</>
);
};
const Page3: React.FC<{ title: string }> = (props) => {
console.log("Page3 ----------------------------------------------");
return (
<>
<div className="container">
<h1 className="title">{props.title}</h1>
<div className="field">
<div className="control">
<Link to="/" className="subtitle">
<FontAwesomeIcon icon={faHome} size="2x" />
Home
</Link>
</div>
</div>
</div>
</>
);
};
const App: React.FC = () => {
const [items, setItems] = useState([routes[0]]);
const [current, setCurrent] = useState(routes[0]);
const update = (items: RouterItem[], current: RouterItem) => {
setItems(items);
setCurrent(current);
};
return (
<BrowserRouter>
<Switch>
<BreadcrumbContext.Provider
value={{
items,
current,
update,
}}
>
<Nav />
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</BreadcrumbContext.Provider>
</Switch>
</BrowserRouter>
);
};
export default App;