Polymorphic components
Polymorphic components (다형성 컴포넌트)
component prop으로 루트 요소를 바꿀 수 있는 다형성(polymorphic) 컴포넌트에 대해 설명해 드릴게요. renderRoot prop, 제네릭 컴포넌트, 래퍼 컴포넌트 만들기, 나만의 다형성 컴포넌트 생성까지 다뤄요.
출처: 문서
본문
다형성 컴포넌트란?
다형성 컴포넌트는 component prop으로 루트 요소를 바꿀 수 있는 컴포넌트예요. 모든 다형성 컴포넌트에는 component prop이 제공되지 않을 때 사용되는 기본 요소가 있어요. 예를 들어 Button 컴포넌트의 기본 요소는 button이고, a나 다른 요소/컴포넌트로 바꿀 수 있어요:
import { Button } from '@mantine/core';
function Demo() {
return (
<Button component="a" href="https://mantine.dev/" target="_blank">
Mantine website
</Button>
);
}
renderRoot prop
renderRoot는 component prop의 대안으로, React 요소를 반환해야 하는 함수를 받아요. component prop을 사용할 수 없는 경우에 유용해요. 예를 들어 component에 전달하려는 컴포넌트가 제네릭(타입을 받거나 props에서 추론하는 경우, 예: <Link />)일 때요.
renderRoot prop 사용 예시(이전 데모와 결과가 같아요):
import { Button } from '@mantine/core';
function Demo() {
return (
<Button
renderRoot={(props) => (
<a href="https://mantine.dev/" target="_blank" {...props} />
)}
>
Mantine website
</Button>
);
}
!important props 인자를 루트 요소에 스프레드해야 해요. 그렇지 않으면 스타일이 없고 컴포넌트가 접근 가능하지 않을 수 있어요.
다른 React 컴포넌트로 다형성 컴포넌트 만들기
component prop에 다른 React 컴포넌트를 전달할 수 있어요. 예를 들어 react-router-dom의 Link 컴포넌트를 전달할 수 있어요:
import { Link } from 'react-router-dom';
import { Button } from '@mantine/core';
function Demo() {
return (
<Button component={Link} to="/react-router">
React router link
</Button>
);
}
Next.js Link로 다형성 컴포넌트 만들기
Next.js 링크는 모든 Next.js 버전에서 다른 비슷한 컴포넌트와 같은 방식으로 동작하지 않아요.
Next.js 12 이하:
import Link from 'next/link';
import { Button } from '@mantine/core';
function Demo() {
return (
<Link href="/hello" passHref>
<Button component="a">Next link button</Button>
</Link>
);
}
Next.js 13 이상:
import Link from 'next/link';
import { Button } from '@mantine/core';
function Demo() {
return (
<Button component={Link} href="/hello">
Next link button
</Button>
);
}
제네릭 컴포넌트로 다형성 컴포넌트 만들기
component prop에 제네릭 컴포넌트는 전달할 수 없어요. 컴포넌트 prop에서 제네릭 타입을 추론하는 것이 불가능하기 때문이에요. 예를 들어 타입이 지정된 Next.js Link를 component prop에 전달할 수 없어요. 컴포넌트 prop에서 href 타입을 추론할 수 없기 때문이에요. 컴포넌트 자체는 올바르게 동작하지만 TypeScript 오류가 생겨요.
제네릭 컴포넌트를 다형성 컴포넌트에서 동작하게 하려면 component 대신 renderRoot prop을 사용하세요:
import Link from 'next/link';
import { Button } from '@mantine/core';
function Demo() {
return (
<Button renderRoot={(props) => <Link href="/hello" {...props} />}>
Typed Next link button
</Button>
);
}
react-router NavLink로 다형성 컴포넌트 만들기
react-router-dom NavLink 컴포넌트의 className prop은 링크에 active 클래스를 추가할 수 있는 함수를 받아요. 이 기능은 Mantine의 component prop과 호환되지 않으므로, 대신 renderRoot prop을 사용할 수 있어요:
import cx from 'clsx';
import { NavLink } from 'react-router-dom';
import { Button } from '@mantine/core';
function Demo() {
return (
<Button
renderRoot={({ className, ...others }) => (
<NavLink
className={({ isActive }) =>
cx(className, { 'active-class': isActive })
}
{...others}
/>
)}
>
React router NavLink
</Button>
);
}
다형성 컴포넌트 래핑
비다형성 컴포넌트는 props 타입에 React.ComponentProps를 포함해요. 여기서 x는 컴포넌트의 루트 요소예요. 예를 들어 Container 컴포넌트는 다형성이 아니에요. 루트 요소가 항상 div라서 props 타입에 React.ComponentProps가 포함돼요.
다형성 컴포넌트는 루트 요소를 바꿀 수 있어서 props 타입에 React.ComponentProps를 포함하지 않아요. props 타입은 컴포넌트가 렌더링된 후에만 추론될 수 있기 때문이에요.
Mantine 다형성 컴포넌트를 위한 비다형성 래퍼 컴포넌트를 만드는 예시:
import { Button, ButtonProps, ElementProps } from '@mantine/core';
const LinkButton = (props: ButtonProps & ElementProps<'a', keyof ButtonProps>) => (
<Button {...props} component="a" />
);
function Demo() {
return (
<LinkButton href="https://mantine.dev" target="_blank">
Mantine website
</LinkButton>
);
}
Mantine 다형성 컴포넌트를 위한 다형성 래퍼 컴포넌트를 만드는 예시:
import { polymorphic, Button, ButtonProps, Group } from '@mantine/core';
interface CustomButtonProps extends ButtonProps {
label: string;
}
// Default root element is 'button', but it can be changed with 'component' prop
const CustomButton = polymorphic<'button', CustomButtonProps>(
({ label, ...others }: CustomButtonProps) => <Button {...others}>{label}</Button>
);
// Default root element is 'a', but it can be changed with 'component' prop
const CustomButtonAnchor = polymorphic<'a', CustomButtonProps>(
({ label, ...others }: CustomButtonProps) => (
<Button component="a" {...others}>
{label}
</Button>
)
);
function Demo() {
return (
<Group>
<CustomButton label="Button by default" color="cyan" />
<CustomButtonAnchor label="Anchor by default" href="https://mantine.dev" target="_blank" />
</Group>
);
}
동적 component prop
component prop에 동적 값을 사용할 수 있어요. 하지만 이 경우 타입을 수동으로 제공하거나, 다형성 컴포넌트에 타입 인자로 any를 전달해 타입 검사를 비활성화해야 해요:
import { Box } from '@mantine/core';
function KeepTypes() {
return (
<Box<'input'>
component={(Math.random() > 0.5 ? 'input' : 'div') as any}
/>
);
}
function NukeTypes() {
return (
<Box<any> component={Math.random() > 0.5 ? 'input' : 'div'} />
);
}
나만의 다형성 컴포넌트 만들기
polymorphic 함수와 Box 컴포넌트를 사용해 새 다형성 컴포넌트를 만들 수 있어요:
import { Box, BoxProps, polymorphic, Group } from '@mantine/core';
interface MyButtonProps extends BoxProps {
label: string;
}
const MyButton = polymorphic<'button', MyButtonProps>(
({ label, ...others }: MyButtonProps) => (
<Box component="button" {...others}>
{label}
</Box>
)
);
function Demo() {
return (
<Group>
<MyButton label="Button by default" />
<MyButton
label="MyButton as anchor"
component="a"
href="https://mantine.dev"
target="_blank"
/>
</Group>
);
}
Mantine 컴포넌트를 다형성으로 만들기
다형성 컴포넌트는 tsserver에 성능 오버헤드가 있어요(런타임 성능에는 영향 없음). 그래서 모든 Mantine 컴포넌트가 다형성 타입을 가지는 것은 아니지만, 모든 컴포넌트는 여전히 component prop을 받아요. 루트 요소를 바꿀 수 있어요.
Mantine 컴포넌트를 다형성으로 만들려면 이전 예시와 같은 방식으로 polymorphic 함수를 사용하세요:
import { polymorphic, Group, GroupProps } from '@mantine/core';
const PolymorphicGroup = polymorphic<'button', GroupProps>(Group);
function Demo() {
return (
<PolymorphicGroup component="a" href="https://mantine.dev" />
);
}
더 알아보기 (Learn more)
- Styles performance — 스타일 성능
- Custom components — 커스텀 컴포넌트