Gatsby + TypeScriptでクライアントサイドのルーティングを実現させる

Gastbyは基本的にsrc/pages以下に配置したファイルなどをベースにページをビルド(事前生成)します。 この方法では例えば外部のAPIデータなど、ビルドのタイミング以外のライフサイクルでURLを生成したい場合には […]

広告ここから
広告ここまで

目次

    Gastbyは基本的にsrc/pages以下に配置したファイルなどをベースにページをビルド(事前生成)します。

    この方法では例えば外部のAPIデータなど、ビルドのタイミング以外のライフサイクルでURLを生成したい場合には使えません。

    ということで、通常のReact SPAのように特定のパスについてのみブラウザ上(クライアントサイド)でルーティングするようにします。

    src/pagesにルーティング先コンポーネントを作る

    クライアントサイドでルーティングする場合、どこかしらのコンポーネントに一度着地させる必要があります。CRA等の場合は、リダイレクトの設定で* -> index.htmlのようにするのが一般的かと思います。

    Gatsbyの場合、src/pages配下にこの着地先のファイルを用意してやりましょう。Routerなどの実装もこのファイルにて行います。

    以下は簡単なサンプルです。

    $ vim src/pages/sites.tsx
    
    import * as React from 'react';
    import { Link } from 'gatsby'
    
    type Props = {
    
    }
    const Component: React.FC<Props> = (props) => (
      <div>
        <h1>Hello site</h1>
        <pre>{JSON.stringify(props)}</pre>
        <Link to='/'>Home</Link>
      </div>
    )
    export default Component

    サーバー側でのルーティンの設定を行う

    続いて、ビルド時に先ほど作成したファイルへ着地するような設定を行います。

    方法1: gatsby-plugin-create-client-pathsプラグインを使う

    もっとも簡単な方法はプラグインを使うことです。

    $ yarn add -D gatsby-plugin-create-client-paths
    $ vim gatsby-config.js
    
      ...
      plugins: [
        `gatsby-pl
        {
          resolve: `gatsby-plugin-create-client-paths`,
          options: { prefixes: [`/sites/*`] },
        },

    この方法の場合、prefixesに指定したパス(例ではsites)と同じファイルが存在する必要がある様子でした。

    方法2: gatsby-node.jsに直接書く

    もう1つの方法はプラグインを使わずに実装する方法です。gatsby-node.jsに以下のように書くことで、方法1と同じことが実現できます。

    exports.onCreatePage = async ({ page, actions }) => {
      const { createPage } = actions
      if (page.path.match(/^\/sites/)) {
        page.matchPath = "/sites/*"
        createPage(page)
      }
    }

    こちらの場合、着地先などを柔軟に設定できます。以下では、projects/xxxでもsrc/pages/sites.tsxに着地させるようにしています。

    exports.onCreatePage = async ({ page, actions }) => {
      const { createPage } = actions
      if (page.path.match(/^\/sites/)) {
        page.matchPath = "/sites/*"
        createPage(page)
    
        // もう一つ作る
        const another = {
          ...page,
          matchPath: "/projects/*"
        })
        createPage(nother)
      }
    }

    TypeScriptで書く場合

    ここをTypeScriptで書く場合、matchPathpathがそれぞれunknown型になっていることに注意が必要です。自分の場合は、string型であることを確認するようにしました。

    import { GatsbyNode, Node, Actions } from "gatsby"
    
    const createClientSitePage = (page: Node, actions: Actions, targetPath: string) => {
      const path = typeof page.path ==='string' ? page.path : ''
      const component = typeof page.component === 'string' ? page.component: ''
      const context = (page.context || {}) as Record<string, unknown>
      const regExp = new RegExp(`^\/${targetPath}`)
      if (!path || !component) return
      if (!path.match(regExp)) return
      actions.createPage({
        path,
        component,
        context,
        matchPath: `/${targetPath}/*`
      })
    }
    
    export const onCreatePage: GatsbyNode['onCreatePage'] = async ({ page, actions }) => {
      createClientSitePage(page, actions, 'sites')
    }

    ここまでで、URLを直接入力しても404にならないようにすることができました。

    次はSPA側でのルーティングを設定します。

    reach-routerをつかう

    Gatsbyではreach-routerというライブラリを推奨している様子です。

    $ yarn add @reach/router

    reach-routerはこのように書きます。

    import * as React from 'react';
    import { Router } from "@reach/router"
    
    const Home: React.FC = () => (
      <h2>Home</h2>
    )
    const Page1: React.FC = () => (
      <h2>Page1</h2>
    )
    const Page2: React.FC = () => (
      <h2>Page2</h2>
    )
    
    const base = '/sites'
    export default () => (
      <Router>
        <Page1 path={`${base}/page1`} />
        <Page2 path={`${base}/page2`} />
        <Home path={base} />
      </Router>
    )

    が、この方法はTypeScriptでエラーが発生します。React Componentにpathという属性はもともと定義されていませんので当然といえば当然です。

    reach-router用のラッパーを作る

    GitHubのIssueを見ているとちょうど良さそうなラッパーがありました。

    const RouterPage = (
      props: { pageComponent: JSX.Element } & RouteComponentProps
    ) => props.pageComponent;
    
    export default () = (
      <Router>
        <RouterPage path={`${base}/page1`} pageComponent={<Page1 />} />
        <RouterPage path={`${base}/page2`} pageComponent={<Page2 />} />
        <RouterPage path={`${base}`} pageComponent={<Home />} />
      </Router>
    )

    個人的には、react-routerっぽい書き方なのでこちらを使う方向になるかなと思います。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark