【React + TypeScript】 Chakra UI の Link と React Router v6 の Link を統合する


ReactでUIライブラリ「Chakra UI 」の Link コンポーネントと ルーティングライブラリ「React Router v6」 の Link コンポーネントを統合したものを、TypeScriptのTSXとして記述する方法をご紹介します。

ライブラリのバージョン

"react": "^17.0.2",
"@chakra-ui/react": "^1.8.7,
"react-router-dom": "6",

結論

React.ComponentProps<typeof コンポーネント名> を使って Chakra UI のLinkの型とReact RouterのLinkの型を割り出して、LinkPropsにまとめます。

Link.tsx (最終結果)
import React from "react"
import { Link as ChakraLink } from "@chakra-ui/react"
import { Link as RouterLink } from "react-router-dom"

type LinkProps = React.ComponentProps<typeof ChakraLink> & React.ComponentProps<typeof RouterLink>

export const Link = ({ children, ...props }: LinkProps) => {
  return (
    <ChakraLink as={RouterLink} {...props}>
      {children}
    </ChakraLink>
  )
}

考え方

公式によると……

公式ドキュメントより転載
// 1. import { Link as ReachLink } from "@reach/router"

// 2. Then use it like this
<Link as={ReachLink} to='/home'>
  Home
</Link>

こちらの公式ドキュメントに載っているものは Reach Router ライブラリのLinkコンポーネントと統合する方法であることと、JavaScriptのJSXでの記述法になっています。

途中経過

この方法にならって、両者の Linkコンポーネント を as を使って ChakraLinkRouterLink としてインポートし、統合します。
この時点では、当然propsの型を指定していないため型エラーとなります。

Link.tsx (途中経過)
import { Link as ChakraLink } from "@chakra-ui/react"
import { Link as RouterLink } from "react-router-dom"

export const Link = ({ children, ...props }) => { // 'children' is missing in props validation
  return (
    <ChakraLink as={RouterLink} {...props}> 
      {children}
    </ChakraLink>
    //プロパティ 'to' は型 '{ children: any; as: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>>; }'にありませんが、
    // 型 'OmitCommonProps<LinkProps & RefAttributes<HTMLAnchorElement>, keyof LinkProps>' では必須です。ts(2741)
  )
}

勘違いでPropsの型にハマる

Chakra UI では、コンポーネントのas引数にHTMLタグ名(a,divなど)を指定すると、そのタグ扱いでDOMが生成されます。

Div.tsx
import { Box } from "@chakra-ui/react"

export const Div = () => {
  return (
    <Box as="div">
      あいうえお
    </Box>
  )
}
表示結果
<div class="css-0">あいうえお</div>

そこで、今回のケースを考えたとき、

ChakraLink as={RouterLink}と書いているので、Chakra UIのLinkがReact RouterのLinkが扱いになる。
だから、Propsの型にはReact RouterのLink用のProps型 LinkProps を引数に指定するのが正解なんだろう」

と直感的に思ってしまいました。
しかし、ここに落とし穴があります。

Link.tsx (失敗例)
import { Link as ChakraLink } from "@chakra-ui/react"
import { Link as RouterLink, LinkProps } from "react-router-dom"

export const Link = ({ children, ...props }: LinkProps) => {
  return (
    <ChakraLink as={RouterLink} {...props}>
      {children}
    </ChakraLink>
  )
}

この統合されたLinkコンポーネントに Chakra UI コンポーネント用のCSSを設定する引数(以下の場合はfontSize)を指定して呼び出すと型エラーになります。

Parent.tsx (親コンポーネント)
import { Link } from "components/link"
export const LogInLink: React.FC = () => {
  return (
    <Link mx="2" fontSize="2rem" to="/auth/login">
      ログイン
    </Link>
    // 型 '{ children: string; mx: string; fontSize: string; to: string; }' を
    // 型 'IntrinsicAttributes & LinkProps' に割り当てることはできません。
    // プロパティ 'mx' は型 'IntrinsicAttributes & LinkProps' に存在しません。
  )
}

これは、統合されたLinkコンポーネントの引数型には、React RouterのLink用のProps型・LinkPropsは指定されているが、Chakra UIのLink用のProps型が含まれていないことが原因です。
よって、両者の引数を受け取れるように、ふたつのLinkコンポーネントのPropsを統合したProps型を作る必要があります。

Link.tsx (最終結果)
import React from "react"
import { Link as ChakraLink } from "@chakra-ui/react"
import { Link as RouterLink } from "react-router-dom"

type LinkProps = React.ComponentProps<typeof ChakraLink> & React.ComponentProps<typeof RouterLink>

export const Link = ({ children, ...props }: LinkProps) => {
  return (
    <ChakraLink as={RouterLink} {...props}>
      {children}
    </ChakraLink>
  )
}

以上で、問題なく外部で両者のLinkの機能を統合したコンポーネントを使えるようになりました。