challenges

React Components: Part 3 (Solution)

You can find a summary of the code updates in this pull request . Read on for explanations.

1. Name the un-destructured props

To collect all the props that aren’t explicitly destructured, use this syntax (as described by MDN ):

const { variant, children, ...delegated } = props;

...delegated is not a special name; you can use whatever variable name you wish instead (for example, ...theRestOfTheProps). However, ...delegated is conventional because these props are being “delegated” from the component to its top level element.

Destructuring the functional component argument looks like this:

Button.tsx

function Button({
  variant = "filled",
  children,
  ...delegated
}: ButtonProps) {
  // ...
}

An example

With this syntax, if the component declaration looks like this:

<Button 
  variant="outline"
  type="reset"
  id="my-special-button"
>
  Press me!
</Button>

then delegated would evaulate to an object with all the props except the explicitly declared variant and children:

{ type: "submit", id: "my-special-button" }

2. Delegate props

The syntax for passing along the un-destructured props is (perhaps confusingly) similar. The delegated object can be spread into individual props, like this:

Button.tsx

function Button({
  variant = "filled",
  children,
  ...delegated
}: ButtonProps) {
  return (
    <button 
      className={styles.wrapper} 
      {...delegated}
    >
      {children}
    </button>
  );
}    

(reference: React docs for props spreading )

Example return value

For the same example <Button> declaration example above, the equivalent attributes in the return value would look like this:

<button 
  className={styles.wrapper} 
  type="reset"
  id="my-special-button"
>
  Press me!
</button>

3. Props conflicts

Note that {...delegated} is specified after all the other props. That means the values from delegated will have the last word over any props that were declared previously. Consider this example:

Button.tsx

<button 
  className={styles.wrapper}
  {...delegated}>
  {children}
</button>

Let’s say Button was declared like this (other props omitted for brevity):

<Button className="snazzy">
  please, I want to be pressed
</Button>

Then the component would render as:

<button class="Button_wrapper__rpDYh" class="snazzy">
  please, I want to be pressed
</button>

(Note, the rpDYh string will vary, but Button_wrapper__<random-string> is the general format of CSS modules rendered class names.)

There are two class attributes here:

  1. "Button_wrapper__rpDYh", from the attribute in the returned button element, and
  2. "snazzy" (from the Button declaration).

Guess which one’s going to win? The second one. The second attribute will clobber the first one, as though we were living in a world where class="Button_wrapper__rpDYh" never existed.

The way to remedy this is to explicitly destructure any props that will clash with props you are using, and combine the values. Here’s how that looks with className (using clsx for ease of class combining):

Button.tsx

import clsx from "clsx";
// ...
 
function Button({
  variant = "filled",
  className,
  children,
  ...delegated
}: ButtonProps) {
  return (
    <button 
      className={clsx(styles.wrapper, className)}
      {...delegated}>
      {children}
    </button>
  );
}    

className is the only potential clash for either of our components at the moment, but we will revisit this concept in a future workshop (when we add a style prop to the Card component’s article element).

Note: All of the work we did for prop delegation in the Button component can (and should!) be applied to the Card component.

Up next

With all the props work done, the next workshop will tackle styling for the Button variants.