Gatsbyで超絶スムーズに動く目次のコンポーネント実装する

こんな感じで、目次をクリックすると、超絶スムーズに目的地に到達する機能を実装します。

実際の動作は本家サイト[genkitech.net](https://genkitech.net/gatsby-table-of-content-scroll)でご覧ください（まだ存在していたら）

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/834797/cbb3f75f-33f1-14d1-8025-3367983f9822.gif)

### やりたかったこと

1. 好きな場所に配置できる目次コンポーネントの作成
2. URLにpage#id のように#を追加せず、今のURLのままジャンプ（選択可）
3. スムーズなスクロールUI



## 作り方

基本的なGatsbyの知識を前提とします。

### 必要なプラグイン

以下のプラグインを使用しているためインストールします。

* gatsby-remark-autolink-headers（hタグにジャンプするためのIDを埋め込む）

* gatsby-plugin-anchor-links（URLを変えずにジャンプするコンポーネントなど）

### gatsbyの設定

gatsby-config.jsに以下の設定を追加します。

```js:gatsby-config.js
...
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          ...
          {
            resolve: `gatsby-remark-autolink-headers`,
            options: {
              icon: false, //Hタグ横のアイコンを消す場合false
            },
          },
          ...
        ]
      }
    },
    {
       resolve: "gatsby-plugin-anchor-links",
       options: {
            offset: -100 //スクロール先のオフセット
       }
    },
...        
```

### コンポーネントを作成する

以上でHタグにIDが埋め込まれ、`<AnchorLink>`コンポーネントを配置するとジャンプするようになったので、以下の目次表示コンポーネントを作成して画面に追加します。

下の方の sx={{}} の所は「theme-ui」の機能を使ってスタイリングしているだけなので、上部のimportと一緒にとりあえず外してしまっても大丈夫です。

```js
/** @jsx jsx */
import { jsx } from "theme-ui"
import React from 'react';
import { graphql, StaticQuery } from 'gatsby';
import { AnchorLink } from "gatsby-plugin-anchor-links";

export const TableOfContents: React.FC<{
  className?: string, 
  slug: string,
}> = props => (
  <StaticQuery
    query={graphql`
      {
        allMarkdownRemark {
          edges {
            node {
              headings {
                depth
                id
                value
              }
              fields {
                slug
              }
            }
          }
        }
        
      }
    `}
    render={(data: any) => {
      //graphqlでは全件取得しているため対象を特定
      const headings = data.allMarkdownRemark.edges.find((n:any)=>{
        return n.node.fields.slug == props.slug })?.node?.headings;

      return (headings && <div sx={{display: 'grid',gap: 2}}>
        {headings.map((x:any)=><AnchorLink         
            className={props.className??'' + ' toc-link toc-depth-'+x.depth}

            //末尾にスラッシュを付ける場合はこっち
            to={"/" + props.slug + '#' + x.id}

            //末尾にスラッシュを付けない場合はこっち
            //to={"/" + props.slug.replace('/','') + '#' + x.id}

            stripHash //#表示を追加しない

            sx={{     //theme-uiでスタイルを設定
              display: 'block',
              ml: ((Number(x.depth)-2)*15)+'px',
              textDecoration: 'none',
              fontSize:'0.95rem',
              lineHeight: '1.1rem',
              '&:hover': { textDecoration: 'underline' }
            }}
          >
            {x.value}
          </AnchorLink>
        )}
      </div>);
    }}
  />
);
```

## 使い方と解説

上の`<TableOfContents>`コンポーネントの例では、slugをキーにして対象のドキュメントを検索するため、post.jsなどの記事を表示しているコンポーネントで以下のようにすると

```js
<TableOfContents slug={post.fields.slug} />
```

好きなところに目次ジャンプ機能を挿入できます。

![image-20201120194331767.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/834797/2ea07e5e-c280-f909-afe0-ac5349d0b0c9.png)

### 補足

好きなところにコンポーネントを配置できるようするため、親からgraphqlの情報を受け取らず、StaticQueryで全件取得して対象だけ使用しています。（gatsbyの画像でよくやる方法と同じです）

ただこの方法は、本番環境では影響がないものの、「gatsby develop」で開発してホットリロードされているときに、ブラウザでF5などで更新すると、develop開始時の情報に見た目上戻ってしまう事があったりします。

再びホットリロードさせるか、developコマンドを再実行すると最新情報で表示されるようです。