challenges

React Components: Part 2 (Solution)

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

1. Card props

Let’s start with the props specified for the Card component (we’ll get to children in a minute). These will both be typed as strings. We could do some extra checking on the string format with a tool like zod (for example, verifying HEX or HSL or other valid CSS color formats), but that’s outside the scope of this workshop.

Card.tsx

export interface CardProps {
  textColor: string;
  backgroundColor: string;
};

2. Button props (literal type)

The spec described two possible values for the Button variant prop: “filled” and “outline”. This is a good case for a literal type that explicitly specifies the restricted values.

Button.tsx

export interface ButtonProps {
  variant: "filled" | "outline";
};

3. Optional prop with default

variant is supposed to be an optional property, and should default to “filled” if no value is specified. Here’s what that looks like for Button:

Button.tsx

export interface ButtonProps {
  variant?: "filled" | "outline";
};
 
function Button({
  variant = "filled"
}: ButtonProps) {
  return (
    <div className={styles.wrapper}>
    </div>
  );
}

Note the ? in the type declaration (see TypeScript docs for optional properties ) and the default value specified on destructuring (see MDN default values for destructuring assignment )

4. Semantic HTML

Now let’s update the default div top-level element (which came from the new-component template code) to something more descriptive, using semantic HTML .

For the Button component, the button element is the best choice (since this component’s job is to add styles to a standard button).

Button.tsx

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

The element choice isn’t as clear for the Card component. Given the options, article seems like the best fit to me, since a card falls under the “independent item of content” description in the MDN docs .

Card.tsx

function Card({
  textColor,
  backgroundColor,
}: CardProps) {
  return (
    <article className={styles.wrapper}>
    </article>
  );
}

5. children prop

Each of these components will take children , and then render the children within a top-level element. For example, if someone declared:

<Button>Press me!</Button>

The children block would be the string “Press me!“. We want to be able to include the children when we render the component.

For the children prop type, we can use React.ComponentProps (as described in Matt Pocock’s article ).

React.ComponentProps takes an argument, a string representing the element name for which we want to find props (for example, React.ComponentProps<"button">). For now, since we’re only destructuring children, that might not seem needed (in fact, using ComponentProps instead of defining a single children prop may seem like overkill). However, in the future, React.ComponentProps will be useful to be able to accept all the possible props for a particular element (for example, type or onClick for button). This is a real advantage to using ComponentProps for the type.

To add all of the props for the element in question (including children), we can extend the interface (as described in the Matt Pocock articles on ComponentProps and type vs interface for React props ).

Button.tsx

export interface ButtonProps 
  extends React.ComponentProps<"button"> {
    variant?: "filled" | "outline";
  };

Card.tsx

export interface CardProps 
  extends React.ComponentProps<"article"> {
    textColor: string;
    backgroundColor: string;
  };

Then we can use the children prop in the returned JSX:

Button.tsx

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

Card.tsx

function Card({
  textColor,
  backgroundColor,
  children,
}: CardProps) {
  return (
    <article className={styles.wrapper}>
      {children}
    </article>
  );
}

In the next workshop

What if someone wants to specify, say, type="submit" for our Button component? The best user experience would be that the user can pass any button prop to Button (for example, <Button type="submit">Press me!</Button>) and that prop-plus-value would get passed along to the button element at the top level of the Button component.

Do we need to destructure every possible prop and then explicitly add them to the button element? Thank heavens, no. We’ll talk about delegating props in the next workshop.

Workshops in this series

  1. Setup: files and shadows
  2. Props and TypeScript
  3. Delegated props
  4. Variants: Button component
  5. Centering: Card component (coming July 18, 2024)
  6. Animations: loading spinner (coming July 25, 2024)