0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

完成イメージ

image.png
image.png
image.png

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

前提

  • デザイン
    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;
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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?