ReactとパンくずリストとContextAPI

この記事は [React Advent Calendar 2019](https://qiita.com/advent-calendar/2019/react) の 9日目の記事です。

パンくずリストを ContextAPI で実装したら、使い方の勉強になったので実装方法をまとめました。

Next.js でデモを実装しているので、他の環境で利用する場合は少し置き換えが必要ですが、基本部分は変更せずに再利用可能です。

[t-yng/react-breadcrumb-example](https://github.com/t-yng/react-breadcrumb-example)

<b>デモ</b>
[![Edit react-breadcrumb-demo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/t-yng/react-breadcrumb-example/tree/master/?fontsize=14&hidenavigation=1&theme=dark)

## 目次

* [パンくずリストコンポーネントの作成](#パンくずリストコンポーネントの作成)
* [Contextの作成](#Contextの作成)
* [Providerの作成](#Providerの作成)
* [Hooks関数の作成](#Hooks関数の作成)
* [使い方](#使い方)

## パンくずリストコンポーネントの作成

コンポーネントの描画イメージです

<img width="201" alt="スクリーンショット 2019-12-08 18.53.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54838/559fe96f-3809-6a4b-d36a-f687d2354151.png">

`reduce()` で渡されたパンくずリストの要素の一覧を セパレーター（`>`） で結合をしています。

`Link`コンポーネントは Next.js 依存なので、ここだけ自分の環境で適宜置き換えてください。

```tsx

import React, { FC } from 'react';
import Link from 'next/link';

export interface BreadcrumbItem {
    id: number;
    text: string;
    href?: string;
}

interface BreadcrumbProps {
    items: BreadcrumbItem[];
    separator?: string;
}

const Breadcrumb: FC<BreadcrumbProps> = ({ items, separator = '>' }) => {
    return (
        <>
            {
                items
                .map(item =>
                    item.href != null
                        // next/link を利用しているので、環境に合わせて書き換えが必要
                        ? <Link href={item.href}>{item.text}</Link>
                        : <span>{item.text}</span>
                )
                .reduce((prev, curr) => prev.length === 0 ? [curr] : [...prev, ` ${separator} `, curr], [])
            }
        </>
    )
}

export default Breadcrumb;
```

## Contextの作成

パンくずリストの一覧を設定するための `setBreadcrumbItems()` を定義した `BreadcrumbContext` を作成します。
ここではインターフェースを定義しているだけで、中身の実装は次の `BreadcrumbProvider` で行なっていきます。

```tsx

import React from 'react';
import { BreadcrumbItem } from './Breadcrumb';

interface BreadcrumbContext {
    setBreadcrumbItems: (items: BreadcrumbItem[]) => void;
}

export const BreadcrumbContext = React.createContext({} as BreadcrumbContext);
```

## Providerの作成

上で作成したコンポーネントとContextを組み合わせてパンくずリストを表示する機能を提供するコンポーネントです。

データの流れとしては

1. 子コンポーネントが渡された `BreadcrumbContext` の `setBredcrumbItems()` にパンくずリストの一覧を指定して呼び出す
2. ここで定義されている `setBreadcrumbItems()` が実行され `items` の一覧が更新される
3. `Breadcrumb` コンポーネントに更新された `items` が渡されてパンくずリストが描画される

となります。

ページ遷移での描画が残るのを防ぐために、ページ遷移のイベントに合わせて一覧を初期化しています。
`next/router`を利用しているので、環境に合わせて書き換えてください。

```typescript
import React, { useState, FC, HTMLAttributes, useEffect } from 'react';
import { useRouter } from 'next/router';
import { BreadcrumbContext } from './BreadcrumbContext';
import Breadcrumb, { BreadcrumbItem } from './Breadcrumb';

export const BreadcrumbProvider: FC<HTMLAttributes<HTMLElement>> = ({
    children,
}) => {
    const router = useRouter();
    const [items, setItems] = useState([] as BreadcrumbItem[]);

    // ページ遷移をした時にパンくずリストを初期化する
    useEffect(() => {
        // next/router を利用しているので、環境に合わせて書き換えが必要
        router.events.on('routeChangeComplete', () => setItems([]));
    }, []);

    const setBreadcrumbItems = (_items: BreadcrumbItem[]) => {
        // コンポーネントの再帰的な描画を防ぐために空の時だけセットする
        if (items.length === 0) {
            setItems(_items);
        }
    };

    return (
        <>
            <BreadcrumbContext.Provider value={{ setBreadcrumbItems }}>
                {<Breadcrumb items={items} />}
                {children}
            </BreadcrumbContext.Provider>
        </>
    );
};
```

## Hooks関数の作成

`useContext()`をラップした関数を作成します。
子コンポーネントは `BreadcrumbContext` を直接参照せずに、この関数を経由してContextを利用します。

`useContext()`を直接呼び出しても問題が無いですが、利用側が中の実装を意識する必要があり気持ち悪いのと、contextの取得後に何か処理を追加したい時に取り回しやすくなるので、面倒がらずにラップしておくことをオススメします。

```tsx

import { useContext } from 'react';
import { BreadcrumbContext } from './BreadcrumbContext';
import { BreadcrumbItem } from './Breadcrumb';

export const useBreadcrumb = (items: BreadcrumbItem[]) => {
    const context = useContext(BreadcrumbContext);
    context.setBreadcrumbItems(items);
};
```

## 使い方

### ルートコンポーネント

```tsx

export const RootComponent = ({children}) => (
    <BreadcrumbProvider>
        {children}
    </BreadcrumbProvider>
)
```

### 子孫コンポーネント

```tsx

export const ChildComponent = {
    useBreadcrumb([
        {
            id: 1,
            text: 'Home',
            href: '/'
        },
        {
            id: 2,
            text: 'About',
        }
    ]);

    return <h1>This is About Page</h1>
}
```

### 参考

[schiehll/react-alert](https://github.com/schiehll/react-alert)