🐸

簡単Flexboxコンポーネントでコードを綺麗にしよう

2023/12/22に公開

目的

ReactやNext.jsを使用してCSS, CSS in JS, TailwindCSSを適用する際、次のような状況に遭遇したことはありませんか?

export const Hoge = () => {
  return (
    <div className="hoge1">  {/* これにFlexboxを当てたい! */}
      <div className="hoge2">  {/* これにもFlexboxを当てたい!! */}
	<div className="hoge3">  {/* これにもFlexboxを当てたいいいいいい!! */}
	  {/* 複数の要素が続く */}
	</div>
	{/* 複数の要素が続く */}
      </div>
      {/* 複数の要素が続く */}
    </div>
  )
}

すべて同じスタイルであれば、単一のクラスやスタイルを再利用できますが、レイアウトや間隔が異なる場合、個別にスタイルを定義する必要があります。これは非常に手間がかかります。

また、TailwindCSSを使用すると、以下のような複雑なシナリオに直面することがあります。(クラス名は例示のためにちょっと💩めに作成しています)

export const Hoge = () => {
  return (
    {/* ふーんFlexboxか */}
    <div className="flex justify-center gap-2">
      {/* おや...? */}
      <div className="flex flex-col items-center flex-wrap gap-4 bg-white border-gray-200 shadow-lg"> 
        {/* あ、あああ、あああああ、ああああああああああ */}
	<div className="flex justify-center gap-2 flex-wrap shrink-0 w-1/3 bg-white border-gray-200 shadow-lg sm:flex-col hover:bg-gray-200 sm:text-sm sm:w-2/3">
	  {/* 複数の要素が続く */}
	</div>
	{/* 複数の要素が続く */}
      </div>
      {/* 複数の要素が続く */}
    </div>
  )
}

このような状況では、div要素のスタイルやレスポンシブデザイン、擬似クラスまで考慮すると、classNameの長さが無限に伸びることになります。Lintや適切な改行で可読性は多少向上しますが、レイアウトと修飾を分けることが理想的です。

提案

以下のようなコンポーネントを考えてみました。

Flexbox.tsx
import { forwardRef, HTMLAttributes, ReactNode } from "react";

type divProps = NonNullable<JSX.IntrinsicElements["div"]["style"]>;

export type FlexboxProps = {
  children?: ReactNode;
  style?: JSX.IntrinsicElements["div"]["style"];
  className?: HTMLAttributes<HTMLLIElement>["className"];
  margin?: divProps["margin"];
  padding?: divProps["padding"];
  flexDirection?: divProps["flexDirection"];
  flexWrap?: divProps["flexWrap"];
  justifyContent?: divProps["justifyContent"];
  alignItems?: divProps["alignItems"];
  alignContent?: divProps["alignContent"];
  flexBasis?: divProps["flexBasis"];
  flexGrow?: divProps["flexGrow"];
  flexShrink?: divProps["flexShrink"];
  width?: divProps["width"];
  height?: divProps["height"];
  overflow?: divProps["overflow"];
  gap?: divProps["gap"];
};

export const Flexbox = forwardRef<HTMLDivElement, FlexboxProps>(
  (props, ref) => {
    const { children, style, className, ...stylePlops } = props;
    return (
      <div
        ref={ref}
        className={className}
        style={{ display: "flex", ...stylePlops, ...style }}
      >
        {children}
      </div>
    );
  }
);

Flexbox.displayName = "Flexbox";

type divProps = NonNullable<JSX.IntrinsicElements["div"]["style"]>を用いて、divに適用できるスタイルを取得し、Flexbox関連のCSSをpropsとして抽出しています。

また、classNameの型はHTMLAttributes<HTMLLIElement>["className"]で定義しています。(string型でもいいと思います)

ちなみに:inputやbuttonの属性(props)を一括で受け取る方法

InputやButtonコンポーネントで、onClickやonChangeなどの細かい属性を個別に定義するのは面倒ですが、以下のように一括で定義することが可能です。

type Props = JSX.IntrinsicElements['input']

必要なpropsがあれば、以下のように追加定義できます。

type Props = JSX.IntrinsicElements['button'] & {
  variant: "primary" | "secondary";
  size: "lerge" | "medium" | "small";
  label: string;
}

実装するならこんな感じになります。
これで普通にonClickであったり好きなタグの属性を当てることができます。

Button.tsx
// classnamesでもいいです
import clsx from 'clsx';

type Props = JSX.IntrinsicElements['button'] & {
  variant: "primary" | "secondary";
  size: "lerge" | "medium" | "small";
}

export const Button = ({variant, size, label, ...rest}: Props) => {
  const variantStyle = ... // propsから適当に生成してください
  const sizeStyle = ... // propsから適当に生成してください
  
  return (
    <button className={clsx(variantStyle, sizeStyle)} {...rest}>
      {label}
    </button>
  )
}
実際に使う場合
<Button variant="primary" size="medium" onClick={handleClick}>
  Click Me
</Button>

使用例

Flexboxコンポーネントを使用することで、スタイルやクラスを切り出すことなく、簡単にレイアウトを行うことができます。

export const Hoge = () => {
  return (
    <Flexbox 
      justifyContent={"center"}
      gap={2}
    >
      <Flexbox
	flexDirection={"column"}
	alignItems={"center"}
	flexWrap={"wrap"}
	gap={4}
        className="bg-white border-gray-200 shadow-lg"
      > 
	<Flexbox
	  justifyContent={"center"}
	  flexWrap={"wrap"}
	  flexShrink={0}
	  className="w-1/3 bg-white border-gray-200 shadow-lg sm:flex-col hover:bg-gray-200 sm:text-sm sm:w-2/3">
	  {/* 複数の要素が続く */}
	</Flexbox>
	{/* 複数の要素が続く */}
      </Flexbox>
      {/* 複数の要素が続く */}
    </Flexbox>
  )
}

おわりに

この記事がFlexboxコンポーネントの利用を通じて、わかりやすく効率的なレイアウト方法を提供することを願っています。CSSやCSS in JSでのレイアウトが容易になりますので、レイアウト専用のコンポーネントを探している方は、ぜひこのFlexboxコンポーネントをお試しください。

Discussion