Skip to content
Chapter 22Lesson 6

Quiz - Components and composition

Quiz progress

0 / 0

You wrap a native <button> and want every standard button attribute — disabled, type, onClick, aria-label — to reach it without re-typing each one, while merging the caller’s className with your own. Which body is correct?

type Props = ComponentProps<'button'> & { variant?: 'primary' | 'ghost' };
const Button = ({ variant = 'primary', className, ...rest }: Props) => (
<button className={cn(buttonClasses({ variant }), className)} {...rest} />
);
type Props = ComponentProps<'button'> & { variant?: 'primary' | 'ghost' };
const Button = ({ variant = 'primary', ...rest }: Props) => (
<button {...rest} className={cn(buttonClasses({ variant }))} />
);
type Props = HTMLAttributes<HTMLButtonElement> & { variant?: 'primary' | 'ghost' };
const Button = ({ variant = 'primary', className, ...rest }: Props) => (
<button className={cn(buttonClasses({ variant }), className)} {...rest} />
);

A component needs a prop that is either an href (a link) or an onClick (a button), but never both and never neither. Which typing actually enforces that at compile time?

A discriminated union: { href: string; onClick?: never } | { onClick: () => void; href?: never }.
Two optionals, href?: string and onClick?: () => void, plus a runtime check in the body.
A variant union: variant: 'link' | 'button' with href and onClick both optional.

You’re designing a <Dialog> that owns a header, a body, and a footer, and consumers need to reorder them and occasionally drop the footer. A teammate proposes <Dialog header={...} body={...} footer={...} />. What’s the senior call?

Ship a compound family — <Dialog><DialogHeader>…</DialogHeader>…</Dialog> — so a new region is a new export, and consumers control order and presence in JSX.
Keep the three props but add an order prop and a hideFooter boolean to recover the flexibility.
Pass all three regions as a single children prop and let the dialog parse them by position.

A badge should appear only when there are unread messages: {unreadCount && <Badge>New</Badge>}. With unreadCount at 0, what renders, and what’s the fix?

A literal 0 prints — 0 is falsy so && short-circuits to the number 0, which is renderable. Fix it with a real boolean on the left: unreadCount > 0 && ….
Nothing renders — 0 is falsy, so the whole expression drops out cleanly. No fix needed.
A runtime error — && can’t combine a number with a JSX element. Wrap the count in String().

<Button> needs to sometimes render a real <button> and sometimes a navigation <a> (a Next.js <Link>), without rebuilding its variant table. Why is asChild + Radix Slot the course’s pick over a generic as prop?

The consumer brings the element as a normal child, so it stays typed by its own props; the component only contributes classes and behavior via Slot. An as prop forces the component to retype its whole surface through conditional generics, degrading inference and producing unreadable errors.
asChild renders faster because Slot skips a DOM node, whereas as always adds a wrapper element.
as is deprecated in React 19, so asChild is the only option that still type-checks.

A caller writes <Button variant="primary" className="bg-destructive">Delete</Button>. The variant table emits bg-primary. With the component built as cn(buttonVariants({ variant }), className), what ships to the DOM and why?

Only bg-destructivecn() runs tailwind-merge, which sees the two classes target the same property, keeps the last (the caller’s), and drops bg-primary.
Both bg-primary and bg-destructivecn() concatenates classes, and the cascade decides which wins at runtime.
Only bg-primary — the component’s own variant classes always take precedence over a caller override.

You’re migrating a React 19 <Input> to accept a ref. Which signature is the modern, correct form?

const Input = ({ ref, ...props }: ComponentProps<'input'>) => (
<input ref={ref} {...props} />
);
const Input = forwardRef<HTMLInputElement, ComponentProps<'input'>>(
(props, ref) => <input ref={ref} {...props} />,
);
type Props = ComponentProps<'input'> & { ref: Ref<HTMLInputElement> };
const Input = ({ ...props }: Props) => <input {...props} />;

A ref callback wires up an IntersectionObserver on a node. In React 19, how do you tear it down when the node unmounts?

return a cleanup function from the callback — React runs it on detach, and no longer re-invokes the callback with null.
Branch on the argument: when React calls the callback again with null, disconnect the observer there.
Pair the callback ref with a useEffect that returns observer.disconnect() in its cleanup.

A fixed inset-0 modal nested inside a card with -translate-y-0.5 is clipped to the card and sized against it, not the viewport. Why does a portal to document.body fix it when raising z-index doesn’t?

The card’s transform makes it the containing block for position: fixed; portaling the modal’s DOM under <body> removes every transformed/overflow ancestor from its chain, so fixed measures against the viewport again. z-index can’t undo a containing-block trap.
The card’s overflow: hidden lowers the modal’s stacking order; a portal resets z-index to the top of the page.
A portal applies isolation: isolate, which lifts the modal out of the card’s stacking context.

A parent <div> has onClick, and renders a portaled <button onClick={…}> into document.body. The user clicks the button. Whose handlers run?

Both — the button’s handler, then the parent’s. Portals move the DOM but events bubble through the React tree, where the button is still the div’s child.
Only the button’s — its DOM lands under <body> as a sibling of the div, so the click can’t bubble to the parent.
Only the parent’s — React re-targets portaled clicks to the nearest tree ancestor that owns a handler.

Quiz complete

Score by topic