【React】`as` propsでいろんなタグになるコンポーネント

## 序盤

Reactで開発していて、汎用的なコンテナコンポーネントを実装するときなど、[Chakra UI](https://v2.chakra-ui.com/)の一部コンポーネントにある`as` propsみたいな実装をしたいと思うことがあります。
- 例えば、[TailwindCSS](https://tailwindcss.com/)で`flex flex-row gap-2 items-center justify-between`みたいなクラスを指定した`FlowBox`コンポーネントを作ったとして、「基本は`div`でいいけど、`main`、`header`、`footer`、`article`、`section`でもレンダリングできるようにしたいなあ」というようなケース

毎回調べてる気がするので、備忘録がてら記事にしました。

## 実装

```tsx:Box.tsx
import { ElementType, ComponentPropsWithoutRef } from 'react';

interface Props<T extends ElementType> {
  as?: T;
}

export const Box = <T extends ElementType = 'div'>(
  props: Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>
) => {
  const { children, as: Component = 'div', ...attributes } = props;
  return <Component {...attributes}>{children}</Component>;
};
```
`ComponentPropsWithoutRef<T>`は`T`要素のrefを除いたpropsを示す型ですが、そこから`Props`で独自に定義した`props`を`Omit<>`で落とします。

```tsx:App.tsx
import { ReactNode } from 'react';
import './App.css';
import { Box } from './Box';

// Reactコンポーネント
const MyComponent = (props: { children: ReactNode }) => {
  return (
    <div>
      <p>MyComponentで{props.children}</p>
    </div>
  );
};

function App() {
  return (
    <>
      <Box />
      <Box>{'デフォルトは<div>'}</Box>
      <Box as="h1">{'<h1>でレンダリング'}</Box>
      <Box as="button" onClick={() => alert('ボタン')}>
        {'<button>でレンダリング'}
      </Box>
      <Box as={MyComponent}>{'レンダリング'}</Box>
    </>
  );
}

export default App;
```

### 結果
こうなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524683/b5aa608b-12f1-6e7f-fe7b-062c58c90d21.png)

ソース
![スクリーンショット 2024-04-26 20.36.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524683/caa3ea26-e8af-bfe9-b5b2-5669e4d19000.png)

## 特定のタグだけにしたい

これだとあらゆるタグ|コンポーネントにできてしまうので、特定のタグだけに絞ります。

```tsx:Block.tsx
import { ComponentPropsWithoutRef } from 'react';

// 許容されるタグ名をUnionで。
// Reactの関数コンポーネントを許容したい場合は、`typeof {Component}`でいけます。
type AllowedElementType =
  | 'div'
  | 'main'
  | 'header'
  | 'footer'
  | 'article'
  | 'section';

interface Props<T extends AllowedElementType> {
  as?: T;
}

export const Block = <T extends AllowedElementType = 'div'>(
  props: Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>
) => {
  const { children, as: Component = 'div', ...attributes } = props;
  return <Component {...attributes}>{children}</Component>;
};
```

```tsx
function App() {
  return (
    <>
      <Box />
      <Box>{'デフォルトは<div>'}</Box>
      <Box as="h1">{'<h1>でレンダリング'}</Box>
      <Box as="button" onClick={() => alert('ボタン')}>
        {'<button>でレンダリング'}
      </Box>
      <Box as={MyComponent}>{'レンダリング'}</Box>

      <Block />
      <Block>{'デフォルトは<div>'}</Block>
      <Block as="h1">{'<h1>でレンダリング'}</Block> {/* エラー */}
      <Block as="button" onClick={() => alert('ボタン')}> {/* エラー */}
        {'<button>でレンダリング'}
      </Block>
      <Block as={MyComponent}>{'レンダリング'}</Block> {/* エラー */}
    </>
  );
}
```

### 動かしてみる
https://stackblitz.com/edit/as-props?file=src%2FBox.tsx

## 終わりに
個人で何か作るときに楽できるように、TailwindCSSベースのオレオレUIキットを整備しつつあります。サクッと動かせればいいなら、それこそChakraUIみたいなUIコンポーネント集を使うのが合理的な選択だと思いますが、個人的には特定のコンポーネント集に対する習熟度を高めるということに、あまりモチベーションを持てずにいます。
- それだったらReactそのものを、もっと使いこなせるようになりたいと思っています
- 例外は[Headless UI](https://headlessui.com/)ぐらいかな

自分の好みで、**適切に制限された汎用性**を持ったコンポーネントを作るのが目下の楽しみです。