React(Hooks)によるBreadcrumb(パンくずリスト)実装

# React(Hooks)によるBreadcrumb(パンくずリスト)実装

勉強のためにあえてコンポーネントやライブラリを使わずに独自実装
あくまで勉強用ですので...色々な実装方法もありますので
分割もしていないので見通しの悪いコードですが・・・

### 完成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786643/5ba139e6-4288-1d40-d28e-806ae4e21886.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786643/efe72d13-7919-366c-5a06-5d06d6f3ea68.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786643/a11d5d92-f49b-393d-c51b-107e7985d1d1.png)

こんな感じでパスに応じて画面遷移（しているように）パンくずでナビゲーションします。

### 前提
* デザイン
[Bulma](https://bulma.io/)を使ってます。
* React  
```bash
$ create-react-app sample-app --template typescript
```

### 仕様
* ページ定義は配列で設定（パンくずテキスト、アイコン、パス、表示コンポーネントを定義）
* ルーティングは上記配列から吐き出す
* ナビゲーション部分にパンくずコンポーネントを配置
* パンくずコンポーネントはカスタムフックで表示内容を取得
* パンくず状態はコンテキスト管理

### パンくず設定カスタムフックロジック(useBreadcrumb())

1.Routerのフック(useLocation())を使って、現在のパスを取得

```typescript
  const location = useLocation();
```

2.パスからルート定義配列を引き当て

```typescript
  // 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.パスが「/」の場合、何もしない（処理終了）

```typescript
  // route path is exit
  if (currentPath === routes[0].path) {
    return [[currentItem!], currentItem!];
  }
```

4.既存のパンくずアイテムをPOP

```typescript
  // 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.現在のアイテムを追加

```typescript
  //push current item
  const pushedItems = [...popedItems, currentItem] as RouterItem[];

  // set context value
  breadcrumb.items = pushedItems;
  breadcrumb.current = currentItem!;

```

アイテムをPOPする部分が強引すぎる気が・・・
関連する画面遷移はパスとして「/page1/page2」となっている必要がある・・・

### コード
分割していないので長いですが・・・

```typescript: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;
```