本記事で紹介する汎用コンポーネントとは、ボタンをはじめとした複数の箇所から呼び出されるUIパーツの基本となるコンポーネントのことを指します。
使い回しのしやすい形で汎用コンポーネントを実装することで、必要となる汎用コンポーネントの数を削減できます。
今回は使い回しやすい汎用コンポーネントを実装するために覚えておくとよいテクニックについて紹介します。
検証環境はnext 12.1.4
、react 18.0.0
です。
目次
PropsでコンポーネントのCSSを直接指定する方法
PropsからコンポーネントのCSSを指定できるようにすることで、1つの汎用コンポーネントから複数の派生コンポーネントを生成できるため、CSSのパターンごとにコンポーネントを用意する必要がなくなります。
ここでは汎用ボタンコンポーネントのwidth
・height
・font-size
をPropsで指定する例について紹介します。
方法1: タグのstyle属性に直接指定する(標準環境での実装方法)
Propsから渡された値をタグのstyle
属性に直接記述する方法です。JSX内でstyleを指定するので見栄えがよくないですがCSSライブラリに依存しないのでデフォルトの環境で実装できるというメリットがあります。
実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import styles from "./Button.module.css";
type Props = {
label: string;
onClick: () => void;
width: number;
height: number;
fontSize: number;
};
export const Button: FC<Props> = ({
label,
onClick,
width,
height,
fontSize,
}) => {
return (
<button
className={styles.button}
onClick={onClick}
style={{
width: `${width}px`,
height: `${height}px`,
fontSize: `${fontSize}px`,
}}
>
{label}
</button>
);
};
なお、styleで指定するプロパティが多い場合は変数に設定を外だしするとコードが読みやすくなります。
(略)
export const Button: FC<Props> = ({
label,
onClick,
width,
height,
fontSize,
}) => {
+ const buttonStyle = {
+ width: `${width}px`,
+ height: `${height}px`,
+ fontSize: `${fontSize}px`,
+ };
return (
<button
className={styles.button}
onClick={onClick}
+ style={buttonStyle}
- style={{
- width: `${width}px`,
- height: `${height}px`,
- fontSize: `${fontSize}px`,
- }}
>
{label}
</button>
);
};
src/components/shared/Button.module.css
.button {
min-width: 100px; /* labelの文字列がボタンからはみ出さないように横幅を確保するため、min-widthで横幅を定義している */
padding: 0 25px;
color: white;
font-weight: bold;
background: darkblue;
border-radius: 10px;
cursor: "pointer";
}
コンポーネントの呼び出し例は以下の通りです。
src/components/Example.tsx
import { FC } from "react";
import { Button } from "./shared/Button";
const handleClick = () => {
alert("Hi");
};
export const Example: FC = () => {
return (
<>
<Button
label={"ボタン"}
onClick={handleClick}
width={100}
height={50}
fontSize={15}
/>
<br />
<Button
label={"ボタン"}
onClick={handleClick}
width={200}
height={100}
fontSize={30}
/>
</>
);
};
方法2: Emotionを利用する
Emotionとは比較的最近できたCSS in JSライブラリです。後発ということもあり多機能で使い勝手がよいと評判です。
Emotionの導入方法の詳細はEmotionをNext.js x TypeScriptの環境にインストールする手順で紹介しています。
Emotionを利用した実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import { css } from "@emotion/react";
type Props = {
label: string;
onClick: () => void;
width: number;
height: number;
fontSize: number;
};
export const Button: FC<Props> = ({
label,
onClick,
width,
height,
fontSize,
}) => {
return (
<button css={css(buttonStyle(width, height, fontSize))} onClick={onClick}>
{label}
</button>
);
};
const buttonStyle = (width: number, height: number, fontSize: number) => {
return css({
minWidth: "100px",
width: `${width}px`,
height: `${height}px`,
fontSize: `${fontSize}px`,
background: "darkblue",
padding: "0 25px",
color: 'white',
fontWeight: "bold",
borderRadius: "10px",
cursor: "pointer",
});
};
コンポーネントの呼び出し例は「方法1」と同様のため省略します。
Propsでコンポーネントのパターン(タイプ)を制御する方法
汎用コンポーネントに派生パターンを持たせ、Propsでパターンの指定をできるようにする方法です。
ここでは汎用ボタンコンポーネントのサイズのパターン(大、小)をPropsで制御できるようにする例について紹介します。
方法1: Propsの値によって適用クラスを変更する(標準環境での実装方法)
Propsの値をもとにclassName
に適用するクラス名を変更することでコンポーネントのパターンを切り替える方法です。
実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import styles from "./Button.module.css";
type Props = {
label: string;
onClick: () => void;
size: 'small' | 'large';
};
export const Button: FC<Props> = ({ label, onClick, size }) => {
return (
<button
// sizeが'small'の時: styles.buttonとstyles.smallが適用される
// sizeが'small'ではない時: styles.buttonとstyles.largeが適用される
className={`${styles.button} ${
size === 'small' ? styles.small : styles.large
}`}
onClick={onClick}
>
{label}
</button>
);
};
src/components/shared/Button.module.css
.button {
padding: 0 25px;
color: white;
font-weight: bold;
background: darkblue;
border-radius: 10px;
cursor: "pointer";
}
.small {
font-size: 15px;
height: 50px;
min-width: 100px;
}
.large {
font-size: 25px;
height: 75px;
min-width: 150px;
}
コンポーネントの呼び出し例は以下の通りです。
src/components/Example.tsx
import { FC } from "react";
import { Button } from "./shared/Button";
const handleClick = () => {
alert("Hi");
};
export const Example: FC = () => {
return (
<>
<Button label={"ボタン"} onClick={handleClick} size="small" />
<br />
<Button label={"ボタン"} onClick={handleClick} size="large" />
</>
);
};
方法2: classnamesを使う
classnamesは条件に応じて適用させるクラス名を簡単に変更できるようするライブラリです。
classnamesのインストール方法は以下の通りです。
$ yarn add classnames
$ yarn add --dev @types/classnames
classnamesを利用した実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import cx from "classnames";
import styles from "./Button.module.css";
type Props = {
label: string;
onClick: () => void;
size: "small" | "large";
};
export const Button: FC<Props> = ({ label, onClick, size = "small" }) => {
return (
<button
// sizeが'small'の時: styles.buttonとstyles['small']が適用される
// sizeが'large'の時: styles.buttonとstyles['large']が適用される
className={cx(styles.button, styles[size])}
onClick={onClick}
>
{label}
</button>
);
};
なお、classnamesを利用したクラス名の指定方法はドット形式でも配列形式でも同じ意味を持つので1、styles.button
はstyles['button']
に書き換え可能です。
CSSおよびコンポーネントの呼び出し例は「方法1」と同様のため省略します。
方法3: Emotinon使う
Emotionを利用した実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import { css } from "@emotion/react";
type Props = {
label: string;
onClick: () => void;
size: "small" | "large";
};
export const Button: FC<Props> = ({ label, onClick, size }) => {
return (
<button css={css(buttonStyle(size))} onClick={onClick}>
{label}
</button>
);
};
const buttonStyle = (size: "small" | "large") => {
return css({
minWidth: size === "small" ? "100px" : "150px",
height: size === "small" ? "50px" : "75px",
fontSize: size === "small" ? "15px" : "25px",
background: "darkblue",
padding: "0 25px",
color: "white",
fontWeight: "bold",
borderRadius: "10px",
cursor: "pointer",
});
};
コンポーネントの呼び出し例は「方法1」と同様のため省略します。
補足: コンポーネントのパターンをUnion型で表現する方法について
パターンのUnion型を定義することで未定義のパターンがPropsから渡ってくることを防げます。
TypeScriptではtypeof X[kyeof typeof X]
を利用することでUnion型が定義できます。
typeof X[kyeof typeof X]
の意味についてはTypeScriptの『typeof X[keyof typeof X]』の意味を順を追って理解するで紹介しています。
コンポーネントのパターンをUnion型で表現すると実装は以下のようになります。
src/components/shared/Button.tsx
import React, { FC } from "react";
import { css } from "@emotion/react";
export const Size = {
Small: "small",
Large: "large",
} as const;
// 以下のように推論される
// type const = {
// readonly Small: "small";
// readonly Large: "large";
// }
type Size = typeof Size[keyof typeof Size];
// 以下のように推論される
// type Size = "small" | "large"
type Props = {
label: string;
onClick: () => void;
size: Size;
};
export const Button: FC<Props> = ({ label, onClick, size }) => {
return (
<button css={css(buttonStyle(size))} onClick={onClick}>
{label}
</button>
);
};
const buttonStyle = (size: Size) => {
return css({
minWidth: size === Size.Small ? "100px" : "150px",
height: size === Size.Small ? "50px" : "75px",
fontSize: size === Size.Small ? "15px" : "25px",
background: "darkblue",
padding: "0 25px",
color: "white",
fontWeight: "bold",
borderRadius: "10px",
cursor: "pointer",
});
};
コンポーネントの呼び出し例は以下の通りです。
src/components/Example.tsx
import { FC } from "react";
import { Button, Size } from "./shared/Button";
const handleClick = () => {
alert("Hi");
};
export const Example: FC = () => {
return (
<>
<Button label={"ボタン"} onClick={handleClick} size={Size.Small} />
<br />
<Button label={"ボタン"} onClick={handleClick} size={Size.Large} />
</>
);
};
Propsの真偽値を利用してコンポーネントの内容を制御する方法
真偽値をPropsで受け取ることで「アクティブ/非アクティブ」や「表示/非表示」など、2つの状態をコンポーネントで表現できます。
2つの状態をトグル形式で切り替えたいケースはよくあるため、真偽値の受け渡し方法は汎用コンポーネントを作成する上で覚えておくと良いテクニックです。
ここでは汎用ボタンコンポーネントのボタンの「活性/非活性」をPropsで制御できるようにする例について紹介します。
方法1: Propsの値によって適用クラスを変更する(標準環境での実装方法)
真偽値をもとにclassName
に適用するクラス名を変更することでコンポーネントの状態を切り替える方法です。
真偽値が条件になるため三項演算子を利用した実装になります。
実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import styles from "./Button.module.css";
type Props = {
label: string;
onClick: () => void;
disabled: boolean;
};
export const Button: FC<Props> = ({ label, onClick, disabled }) => {
return (
<button
className={`${styles.button} ${
disabled ? styles.disable : styles.active
}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
src/components/shared/Button.module.css
.button {
font-size: 15px;
height: 50px;
min-width: 100px;
padding: 0 25px;
color: white;
font-weight: bold;
border-radius: 10px;
}
.active {
background: darkblue;
cursor: "pointer";
}
.disable {
background: gray;
}
コンポーネントの呼び出し例は以下の通りです。
src/components/Example.tsx
import { FC } from "react";
import { Button } from "./shared/Button";
const handleClick = () => {
alert("Hi");
};
export const Example: FC = () => {
return (
<>
<Button label={"ボタン"} onClick={handleClick} disabled={false} />
<br />
<Button label={"ボタン"} onClick={handleClick} disabled={true} />
</>
);
};
方法2: classnamesを利用する
classnamesを利用した実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import cx from "classnames";
import styles from "./Button.module.css";
type Props = {
label: string;
onClick: () => void;
disabled: boolean;
};
export const Button: FC<Props> = ({ label, onClick, disabled }) => {
return (
<button
className={cx(styles.button, disabled ? styles.disable : styles.active)}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
CSSおよびコンポーネントの呼び出し例は「方法1」と同様のため省略します。
方法3: Emotionを利用する
Emotionを利用した実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import { css } from "@emotion/react";
type Props = {
label: string;
onClick: () => void;
disabled: boolean;
};
export const Button: FC<Props> = ({ label, onClick, disabled }) => {
return (
<button
css={css(buttonStyle(disabled))}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
const buttonStyle = (disabled: boolean) => {
return css({
minWidth: "100px",
height: "50px",
fontSize: "15px",
background: disabled ? "gray" : "darkblue",
padding: "0 25px",
color: "white",
fontWeight: "bold",
borderRadius: "10px",
cursor: "pointer",
});
};
コンポーネントの呼び出し例は「方法1」と同様のため省略します。
補足: Propsでtrueを渡す方法について
Propsでtrue
を渡す場合={true}
を省略できます。
つまり、例えば以下のコードは同義となります。
// 両方ともdisabledはtrueになる
<Component disabled={true} />
<Component disabled />
ただし、公式ドキュメントの「JSX In Depth#Props Default to “True”」に書かれているように、={true}
を省略するとES6のショートハンドと混同させてしまうため省略しないほうが無難です。
Propsのデフォルト値を用意する方法
デフォルト引数を利用することでPropsのデフォルト値が用意できます。
たとえば汎用ボタンコンポーネントのdisabled
のデフォルト値をfalse
にする実装例は以下の通りです。
src/components/shared/Button.tsx
import React, { FC } from "react";
import { css } from "@emotion/react";
type Props = {
label: string;
onClick: () => void;
disabled?: boolean; // デフォルト値が用意されているのでオプション引数(optional prameter)でよい
};
export const Button: FC<Props> = ({ label, onClick, disabled = false }) => {
return (
<button
css={css(buttonStyle(disabled))}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
const buttonStyle = (disabled: boolean) => {
return css({
minWidth: "100px",
height: "50px",
fontSize: "15px",
background: disabled ? "gray" : "darkblue",
padding: "0 25px",
color: "white",
fontWeight: "bold",
borderRadius: "10px",
cursor: "pointer",
});
};
コンポーネントの呼び出し例は以下の通りです。
src/components/Example.tsx
import { FC } from "react";
import { Button } from "./shared/Button";
const handleClick = () => {
alert("Hi");
};
export const Example: FC = () => {
return (
<>
<Button label={"ボタン"} onClick={handleClick} disabled={false} />
<br />
<Button label={"ボタン"} onClick={handleClick} disabled={true} />
<br />
{/* disabledを指定しない場合、disabledはデフォルト値のfalseとなる */}
<Button label={"ボタン"} onClick={handleClick} />
</>
);
};
参考: 今回紹介したテクニックを利用した汎用ボタンコンポーネント
参考として、今回のテクニックを利用して作成した汎用ボタンコンポーネントのサンプルコードを紹介します。仕様は以下の通りです。
- ボタンサイズは小と大の2パターンがある
- ボタンの色は青、黄、赤の3パターンがある
- withArrowの真偽値で矢印アイコンの表示/非表示を制御する
- disabledの真偽値でボタンの活性/非活性を制御する
- disabledがtrueの場合、ボタンは灰色になる
- ボタンのデフォルトのデザインは「サイズ小、青色、矢印アイコンなし」
なお、矢印アイコンの表示/非表示の制御には&&演算子(論理積演算子、論理結合)を利用しています。
&&演算子による表示制御の詳細解説は【React】JSXにおけるコンポーネントの表示・非表示の制御方法まとめで紹介しています。
src/components/shared/Button.tsx
import React, { FC } from "react";
import { css } from "@emotion/react";
export const Size = {
Small: "small",
Large: "large",
} as const;
export const ButtonType = {
Primary: "darkblue",
Warning: "darkorange",
Danger: "darkred",
} as const;
type Size = typeof Size[keyof typeof Size];
type ButtonType = typeof ButtonType[keyof typeof ButtonType];
type Props = {
label: string;
onClick: () => void;
size?: Size;
buttonType?: ButtonType;
disabled?: boolean;
withArrow?: boolean;
};
export const Button: FC<Props> = ({
label,
onClick,
size = Size.Small,
buttonType = ButtonType.Primary,
disabled = false,
withArrow = false,
}) => {
return (
<button
css={css(buttonStyle(size, buttonType, disabled))}
disabled={disabled}
onClick={onClick}
>
{label}
{withArrow && (
<div css={css(arrowStyle)}>
<span css={css(arrowIconStyle)} />
</div>
)}
</button>
);
};
const buttonStyle = (size: Size, buttonType: ButtonType, disabled: boolean) => {
return css({
minWidth: size === Size.Small ? "100px" : "150px",
height: size === Size.Small ? "50px" : "75px",
fontSize: size === Size.Small ? "15px" : "25px",
background: disabled ? "gray" : buttonType,
padding: "0 25px",
color: "white",
fontWeight: "bold",
borderRadius: "10px",
cursor: disabled ? undefined : "pointer",
position: "relative",
});
};
const arrowStyle = css({
position: "absolute",
right: "10px",
top: "50%",
transform: "translateY(calc(-50% - 1px))",
});
const arrowIconStyle = css({
width: "9px",
height: "9px",
transform: "rotate(45deg)",
display: "inline-block",
borderTop: "2px solid white",
borderRight: "2px solid white",
});
コンポーネントの呼び出し例は以下の通りです。
src/components/Example.tsx
import { FC } from "react";
import { Button, Size, ButtonType } from "./shared/Button";
const handleClick = () => {
alert("Hi");
};
export const Example: FC = () => {
return (
<>
<Button label={"ボタン"} onClick={handleClick} />
<br />
<Button label={"ボタン"} onClick={handleClick} withArrow={true} />
<br />
<Button label={"ボタン"} onClick={handleClick} disabled={true} />
<br />
<Button
label={"ボタン"}
onClick={handleClick}
size={Size.Large}
buttonType={ButtonType.Danger}
/>
</>
);
};
表示結果は以下の通りです。
さいごに
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!