🎇

フロントエンド開発でURLのハードコーディングをやめる

2022/09/28に公開
2

お知らせ
この記事の内容を元にしたpath-kanriというライブラリを公開しました!
https://github.com/koyablue/path-kanri

はじめに

ルーティングのパスをハードコーディングせずに、登録済みのものを関数で呼び出せるようにしたという内容です。

*

最近Next.jsで開発を行なっています。

import { useRouter } from 'next/router'

const ExampleComponent = () => {
  const router = useRouter()
  // (略)
  const randomFunc = () => {
    // (略)
   router.push(`/example/${exampleId}/${slug}`) // <-これ!!!!!!!!!!!
  }
  
  return(
    <div>
      {/* view */}
    </div>
  )
}

こんな感じでパスがハードコーディングされていることがあります。この書き方が増えてくると個人的にちょっと不安になります。ベタ書きが増えるということは、その分変更に弱くなるということです。
パスを一括で管理してベタ書きではなく関数で呼び出せたら便利で安全なのではと思ったので、試行錯誤してみました。

やりたいこと

こういうことがしたいです。

// /example/{exampleId}/{slug} というpathがあるとする

// '/example/1/abcd' という文字列を返す
getPath('example', { exampleId: 1, slug: 'abcd' })

実装

pathの登録

ただのオブジェクトです。ここに書かれているからといってvalidなわけではないのが気になる点ですが、ないならないで最終的に404が返るだろうしそこでエラーハンドリングすればいいやと思っています。
{}で囲まれている部分がパラメーターのプレースホルダーです。

export const pathNameAndUriMap = {
  home: '/',
  example: '/example/{exampleId}/{slug}',
  login: '/login',
  mypage: '/mypage',
} as const

型です。

import { pathNameAndUriMap } from './paths'

import { valueOf } from 'types/util'

export type PathNameAndUriMap = typeof pathNameAndUriMap

export type PathName = keyof typeof pathNameAndUriMap

export type Uri = valueOf<typeof pathNameAndUriMap>

export type PathParams = { [key: string]: string | number }

ちなみにvalueOfというtypeはこちらの記事を参考に実装しました

// オブジェクトのvalueのunion type
export type valueOf<T> = T[keyof T]

関数の実装

正直getActualUriの中に全部書いてもいいんじゃないかと思っていますが、見通しが悪くなるのでgetParamNamesFromRawUriとvalidateParamsを切り出しました。

import { pathNameAndUriMap } from './paths'

import { PathName, PathParams, PathNameAndUriMap, Uri } from './types'

/**
 * パラメーター名を返す
 * ex) getParamNamesFromRawUri('/example/{exampleId}/{slug}') -> ['exampleId', 'slug']
 *
 * @param {Uri} rawUri
 * @return {*}  {string[]}
 */
const getParamNamesFromRawUri = (rawUri: Uri): string[] => {
  const paramNamesWithBrackets = rawUri.match(/{.+?}/g)
  if (paramNamesWithBrackets === null) return []

  return paramNamesWithBrackets.map(paramNameWithBrackets =>
    paramNameWithBrackets.replace(/{|}/g, '')
  )
}
type ParamNames = ReturnType<typeof getParamNamesFromRawUri>

/**
 * URIに適用するパラメーターが正しい名前で正しい個数与えられているか確認
 *
 * @param {ParamNames} paramNames
 * @param {PathParams} params
 * @return {*}  {boolean}
 */
const validateParams = (
  paramNames: ParamNames,
  params: PathParams
): boolean => {
  const paramsKeys = Object.keys(params)
  return (
    paramNames.length === paramsKeys.length &&
    paramsKeys.every(paramKey => paramNames.includes(paramKey))
  )
}

/**
 * パスを取得する
 * getActualUri('example', { exampleId: 1, slug: 'abcd' }) -> '/example/1/abcd'
 *
 * @param {PathName} pathName
 * @param {PathParams} [params]
 * @return {*}  {string}
 */
export const getActualUri = (
  pathName: PathName,
  params?: PathParams
): string => {
  // '/example/{exampleId}/{slug}'
  const rawUri = pathNameAndUriMap[pathName]

  // ['exampleId', 'slug']
  const paramNames = getParamNamesFromRawUri(rawUri)
  if (!paramNames.length) return rawUri

  if (!params) {
    throw new Error(`Missing required parameters for ${rawUri}.`)
  }

  if (!validateParams(paramNames, params)) {
    throw new Error(`Given parameters are not valid for ${rawUri}.`)
  }

  // '/example/{exampleId}/{slug}' -> '/example/1/abcd'
  let pathToReturn = String(rawUri)
  paramNames.forEach(paramName => {
    pathToReturn = pathToReturn.replace(
      `{${paramName}}`,
      String(params[paramName])
    )
  })

  return pathToReturn
}

つかってみる

上記の内容をcustom hookにまとめた想定です。 "はじめに"にあるサンプルコードを書き直してみます。

usePaths.ts
export const usePaths = () => {
  // (略)
  return { getPath } as const
}
import { useRouter } from 'next/router'

// hooks
import { usePaths } from 'hooks/usePaths' 

const ExampleComponent = () => {
  const router = useRouter()
  
  const { getPath } = usePaths()
  
  const randomFunc = () => {
    // (略)
    router.push(getPath('example', { exampleId: 1, slug: 'abcd' }))
  }
  
  return(
    <div>
      {/* view */}
    </div>
  )
}

おわりに

自分が知らないだけでルーティング管理の超便利なライブラリとかあるんでしょうか。あれば即刻そちらに乗り換えたいところです。

余談ですがLaravelのroute()関数にインスパイアされてます。もともとLaravelを使っていたのでLaravelインスパイア系の関数を作りがちです。

Discussion

hanetsukihanetsuki

Next.jsであればpathpidaがおすすめです!

私はよくこちらを利用していますが、他にも良さそうなライブラリがあれば教えていただけますと幸いですー!

koyabluekoyablue

pathpida 知りませんでした 試してみます
ありがとうございます!