🔗

UIライブラリとルーティングFWを組み合わせて型付きLinkを作る

2024/09/15に公開

はじめに

UIライブラリから提供されている、デザイン付きのLinkコンポーネントを使いたい。
でも、フレームワークが提供している型付きのhrefやtoも使いたい!
そんな欲張りな要望を満たすために、どちらも使えるLinkコンポーネントを作ります。

Linkコンポーネントの例

UIライブラリのコンポーネントは、デザインやスタイルのみを提供しており、renderされるタグは標準HTMLの<a>タグとなります。
このままでは遷移がソフトウェアナビゲーションとならず、読み込みによるちらつきが発生してしまいます。
また、ルーティングフレームワークが提供するLinkコンポーネントは、ソフトウェアナビゲーションが実装されており、使用する際にはアプリケーション内のURL以外を指定した際に、型エラーで補足できるようにしてくれるものもありますが、デザインは自分で適用する必要があります。
そこで、コンポーネントライブラリにフレームワークのLinkを実装し、型を付けることで、デザインと遷移、型安全を両立させます。

YamadaUI × Next.js

theme/components/link.ts
import { ComponentStyle, LinkProps } from "@yamada-ui/react";
import NextLink from "next/link";

export const Link: ComponentStyle<"Link", LinkProps> = {
  defaultProps: {
    as: NextLink,
  },
};

Yamada UIでは、as Propにコンポーネントを指定することで、内部でrenderするコンポーネントを変更することができます。
この機能を利用して、Next.jsのLinkコンポーネントを指定します。
指定の抜け漏れが無いように、Theme機能を利用して、すべてのLinkコンポーネントにNext.jsのLinkコンポーネントを指定します。

https://yamada-ui.com/ja/styled-system/theming/customize-theme

Next.jsでは、typedRoutesという機能を有効にすることで全てのLinkコンポーネントに型が付きます。

next.config.mjs
/** @type {import('next').NextConfig} */
export default {
    experimental: {
      typedRoutes: true,
    },
  };

このRoute型を使って、Yamada UIのLinkコンポーネントの型を拡張します。

yamada-ui.d.ts
import type { Component, LinkProps } from "@yamada-ui/react";
import type NextLink from "next/link";
  
declare module "@yamada-ui/react" {
  declare const Link: <T>(
    props: ComponentProps<Component<typeof NextLink<T>, LinkProps>>,
  ) => React.JSX.Element;
}

このようにすればいけそうですが、なぜかLinkの型が上書きできず…。
ライブラリ側の型定義が優先されてしまいました。

yamada-ui.d.ts
import type { ThemeProps } from "@yamada-ui/react";
import type { Route } from "next";
import type { LinkProps as NextLinkProps } from "next/link";
import type { ComponentProps } from "react";

type LinkOptions = {
  /**
   * If `true`, the link will open in new tab.
   *
   * @default false
   */
  isExternal?: boolean;
};

declare module "@yamada-ui/react" {
  interface LinkProps<T = Route>
    extends Omit<NextLinkProps<T>, "as">,
      HTMLUIProps<"a">,
      ThemeProps<"Link">,
      LinkOptions {}
}

型エイリアスであるLinkPropsを上書きすることはできました。
ファイルの名前は**.d.ts形式で、tsconfig.jsonで読み込みさえされれば何でも構いません。

Yamada UI側のLinkOptionsはexportされていないため、バージョンアップなどで新たな型が増えた場合その都度、こちらにも追加していく必要があります。
より良い方法があればご提示ください!

Next.js Link

hrefの型をNext.jsのRouteで上書きすることに成功しました。

YamadaUI × TanStack Router

import { Link as TSRLink } from "@tanstack/react-router";
import { type ComponentStyle } from "@yamada-ui/react";

export const Link: ComponentStyle<"Link"> = {
  defaultProps: {
    as: TSRLink,
  },
};

TanStack Routerでは、LinkComponentという型が提供されており、この型にLinkコンポーネントを渡すことで、型が付いたLinkコンポーネントを作成できます。

yamada-ui.d.ts
import { type LinkComponent } from "@tanstack/react-router";
import { type Component, type LinkProps } from "@yamada-ui/react";

declare module "@yamada-ui/react" {
  const Link: LinkComponent<Component<"a", LinkProps>>;
}

TanStack Router Link

これでtoやparamsを使った遷移を型付きで使えるようになりました!

また、TanStack Routerでは、createLinkという関数でコンポーネントをラップするだけで、型が付くLinkコンポーネントを作成できます。

import { createLink } from "@tanstack/react-router";
import { Button } from "@yamada-ui/react";

export const ButtonLink = createLink(Button);

asのような仕組みを持たないコンポーネントライブラリでも、これを使うことで簡単に型付きLinkコンポーネントを作成できます。

設定しても型が効かない場合は、d.tsファイルを読み込めていないため、以下の設定を追加してください。

tsconfig.json
{
  "compilerOptions": {
    ...
  },
    "include": ["yamada-ui.d.ts"]
}

まとめ

Yamada UIとルーティングフレームワークを組み合わせて、型付きLinkコンポーネントを作る方法を紹介しました。
アプリケーション内のリンクは、どんなに気をつけていても間違いが起こりやすい部分です。
以前はこのような仕組みを構築するのに、外部ライブラリを導入する必要がありましたが、最近ではフレームワークが型安全なリンク機能を提供してくれるようになり、手軽に導入することができるようになりました。
型を使って、リンクの遷移先を安全に管理しましょう!

Discussion