How to create React component properly

  • 2024/11/27
  • How to create React component properly はコメントを受け付けていません

eyecatch

In this post we will take a look at how I created React component in the past and how I changed it. These changes are crucial for creating components efficiently, easy to maintenance and upgrade.

Project initialization

I will use NextJS and Tailwind CSS for the demonstration. You can use below command for setting up the project. NodeJS, npm or yarn are required.

npx create-next-app@latest
#or
yarn create next-app

#classnames library for managing className
npm install -S classnames
#or
yarn add classnames

Component creation

The old way

I’m going to create React button component. This components characteristics are:

  • Not using native element.
  • Not extending native properties.
  • Using too specific properties.
  • Not exporting component properties.
import classNames from "classnames";
import { FC } from "react";

interface ButtonProps { //not exporting and extending button properties
  className?: string;
  text?: string;
  isPrimary?: boolean;
  isSecondary?: boolean; //too specific property for setting color
  onClick?: () => void;
}

export const OldButton: FC<ButtonProps> = (props) => {
  return (
    <div
      className={classNames(
        "cursor-pointer text-sm text-center p-2 rounded",
        {
          "text-white bg-blue-500": props.isPrimary,
          "text-white bg-green-500": props.isSecondary,
        },
        props.className
      )}
      onClick={props.onClick}
    >
      {props.text}
    </div> //not using native element. Div element is used for button
  );
}

The new way

It is recommended to use correct element when possible. Characteristics of the new way are:

  • Using native element.
  • Extending native properties.
  • Using general properties.
  • Exporting component properties.
import classNames from "classnames";
import { ButtonHTMLAttributes, FC } from "react";

export interface NewButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { //exporting and extending button properties
  color?: "primary" | "secondary"; //using general property for setting color
}

export const NewButton: FC<NewButtonProps> = (props) => {
  const {
    color,
    className,
    ...rest
  } = props;

  return (
    <button
      className={classNames(
        "text-sm p-2 rounded",
        {
          "text-white bg-blue-500": color === "primary",
          "text-white bg-green-500": color === "secondary",
        },
        className,
      )}
      {...rest}
    /> //using button element
  );
};

Render and result

Now you can render components.

<OldButton
  className={"px-6"}
  isPrimary={true}
  text={"My old button"}
  onClick={() => console.log("Clicked old button!")}
/>
<NewButton
  className={"px-6"}
  color={"primary"}
  onClick={() => console.log("Clicked new button!")}
>
  My new button
</NewButton>

react component button

As you can see, two components appearances are identical. But the code underneath is different, this affects development time and future extension.

Issue discussion

Using correct component and properties extension

We already have had the onClick() property on both old and new button, but the button is not always clickable. So we will need a property to disable them.
With the old way we need to add “disabled” property and fix the logic.

import classNames from "classnames";
import { FC } from "react";

interface ButtonProps {
  className?: string;
  text?: string;
  isPrimary?: boolean;
  isSecondary?: boolean;
  isDanger?: boolean;
  disabled?: boolean;//new property
  onClick?: () => void;
}

export const OldButton: FC<ButtonProps> = (props) => {
  return (
    <div
      className={classNames(
        "text-sm text-center p-2 rounded",
        {
          "cursor-pointer": !props.disabled,//style for "disabled" property
          "text-white bg-blue-500": props.isPrimary,
          "text-white bg-green-500": props.isSecondary,
          "text-white bg-red-500": props.isDanger,
        },
        props.className
      )}
      onClick={() => {
        //new logic for "disabled" property
        if (!props.disabled && props.onClick) {
          props.onClick();
        }
      }}
    >
      {props.text}
    </div>
  );
}

With the new way we don’t have declare new property for disabling button. We also don’t have to fix cursor style and onClick logic. Button element handles these styles and logic internally.

Meaningful and extensible properties

The old way use two properties “isPrimary” and “isSecondary” for setting the color. This will cause issue if both properties are “true” and you have to add new property for new color like the code below (pseudo code).

isDanger?: boolean;
...
"text-white bg-red-500": props.isDanger,
    

With the new way we only need to add new option for the color and only one color can be set (pseudo code).

color?: "primary" | "secondary" | "danger";
...
"text-white bg-red-500": color === "danger",
    

Exporting properties

The old way does not export its interface. So when you want to add new feature for current component, you have to edit it. For example you want to add an icon on the left of the text (pseudo code).

leftIcon?: ReactNode;
...
{!props.leftIcon && props.text}
{props.leftIcon && (
  <div className={"flex items-center gap-2"}>{props.leftIcon}{props.text}</div>
)}
    

Because the new way exports its interface so you can create a new icon button without editing current component.

import { FC } from "react";
import { NewButton, NewButtonProps } from "./NewButton";

export const NewIconButton: FC<NewButtonProps> = (props) => {
  const { children, ...rest } = props;

  return (
    <NewButton {...rest}>
      <div className={"flex items-center gap-2"}>{children}</div>
    </NewButton>
  );
};

Render two icon buttons and see the result.

<OldButton
  className={"px-6"}
  isSecondary={true}
  text={"My old icon button"}
  leftIcon={
    <img
      src={"/search.svg"}
      width={24}
      height={24}
    />
  }
  onClick={() => console.log("Clicked old icon button!")}
/>
<NewIconButton
  className={"px-6"}
  color={"secondary"}
  onClick={() => console.log("Clicked new icon button!")}
>
  <img
    src={"/search.svg"}
    width={24}
    height={24}
  />
  My new icon button
</NewIconButton>

react component icon button

Conclusion

The result display on browser looks the same for end users but the code is different for developers. I had used the old way for a long time, it’s fine when there aren’t many functions and logics. But nowadays, designers want to custom and put a lot of things into their component design. If we go with the old way, we will have to add a lot of properties and logics.
I’m using the new way now and I found it is convenient and easy to extend. I can make new component by extending current one. Let’s try the new way and see!

関連記事

カテゴリー:

ブログ

情シス求人

  1. チームメンバーで作字やってみた#1

ページ上部へ戻る