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で書く場合、matchPathとpathがそれぞれ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っぽい書き方なのでこちらを使う方向になるかなと思います。