컴포넌트가 렌더링하는 요소를 외부에서 제어하는 세 가지 패턴.

as: 소비자가 태그 이름(as=“a”)을 넘기면 Button 이 Comp = as 로 그 태그를 렌더하고 props 를 그대로 펼친다 — 단순 태그 변경에 쓴다. asChild: 소비자가 자식 요소(<a href />)를 넘기면 Button 이 Slot 으로 그 자식을 cloneElement 해 className 병합·핸들러 합성·ref 병합을 암묵적으로 한다 — 기존 컴포넌트 합성에 쓴다. render: 소비자가 render 에 요소나 (props, state) => 함수를 넘기면 Button 이 ref 포함 DOM props 와 isPressed 같은 상태를 명시적으로 건네고 소비자가 직접 펼친다 — 상태 기반 커스터마이징과 합성에 쓰고 타입 추론이 가장 좋다.

as

type PolymorphicProps<E extends React.ElementType> = {
  as?: E
} & React.ComponentPropsWithoutRef<E>

function Button<T extends React.ElementType = 'button'>({
  as,
  ...props
}: PolymorphicProps<T>) {
  const Comp = as || 'button'
  return <Comp {...props} />
}

asChild + Slot

import { Slot } from '@radix-ui/react-slot'

function Button({ asChild, ...props }) {
  const Comp = asChild ? Slot : 'button'
  return <Comp {...props} />
}

Slot 직접 구현:

const Slot = forwardRef(({ children, ...props }, ref) => {
  if (!isValidElement(children)) return null

  return cloneElement(children, {
    ...props,
    ...children.props,
    ref: ref || children.ref,
  })
})

자식 요소 타입에 따른 조건부 Props:

type ConditionalProps<T> = T extends ReactElement<any, 'a'>
  ? { href?: string; external?: boolean }
  : T extends ReactElement<any, 'button'>
    ? { type?: 'button' | 'submit' }
    : {}

render

// Element 방식
<Button render={<a href="/about" />}>Link</Button>

// Callback 방식 - state 접근 가능
<Button render={(props, state) => (
  <motion.button {...props} animate={state.isPressed ? ... : ...} />
)}>
  Animated
</Button>

`#538`