序盤
Reactで開発していて、汎用的なコンテナコンポーネントを実装するときなど、Chakra UIの一部コンポーネントにあるas
propsみたいな実装をしたいと思うことがあります。
- 例えば、TailwindCSSで
flex flex-row gap-2 items-center justify-between
みたいなクラスを指定したFlowBox
コンポーネントを作ったとして、「基本はdiv
でいいけど、main
、header
、footer
、article
、section
でもレンダリングできるようにしたいなあ」というようなケース
毎回調べてる気がするので、備忘録がてら記事にしました。
実装
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<>
で落とします。
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;
結果
特定のタグだけにしたい
これだとあらゆるタグ|コンポーネントにできてしまうので、特定のタグだけに絞ります。
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>;
};
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> {/* エラー */}
</>
);
}
動かしてみる
終わりに
個人で何か作るときに楽できるように、TailwindCSSベースのオレオレUIキットを整備しつつあります。サクッと動かせればいいなら、それこそChakraUIみたいなUIコンポーネント集を使うのが合理的な選択だと思いますが、個人的には特定のコンポーネント集に対する習熟度を高めるということに、あまりモチベーションを持てずにいます。
- それだったらReactそのものを、もっと使いこなせるようになりたいと思っています
- 例外はHeadless UIぐらいかな
自分の好みで、適切に制限された汎用性を持ったコンポーネントを作るのが目下の楽しみです。