UI コンポーネントにマージンを付けずに実装する - Chakra UI の場合 -

この記事は、[React Advent Calendar 2021](https://qiita.com/advent-calendar/2021/react) 1 日目の記事です。

----

定期的に話題になる、UI コンポーネントでの余白 (margin, padding) をどうするか問題。
昨日も Zenn に解説記事が流れてきました。

https://zenn.dev/ryusou/articles/why-margin-component

(参照記事の参照記事)

https://qiita.com/otsukayuhi/items/d88b5158745f700be534


Zenn の記事の中で、**UI ライブラリーにはレイアウト (Box モデル) が当たり前に存在する** と記載がされています。

(※一部引用)

> 例えば、Chakra UIではレイアウト(Boxモデル)に特化したコンポーネントが存在します。
> Chakraのレイアウトの責務を持ったコンポーネントの豊富さは大変魅力的ですね。

そう、**Chakra UI のレイアウト関連コンポーネントは、大変魅力的なのです・・・！**
ということで、マージンを持たない UI コンポーネントを Chakra UI で作ってみようと思います。


## 保険

React を仕事で使い始めてから 3 ヶ月、Chakra UI は 1 ヶ月程度です。


## Case 1. 保存ボタンコンポーネント (SaveButton)

データを保存するアクションを表す、保存ボタン (SaveButton) コンポーネントを作っていきます。
Atomic Design でいう、Molecules に該当するかなと思います。

コンポーネントの要件は次のとおりとします。

- ボタンは緑色
- 左側にチェックアイコンがついている

### シンプルな実装

とりあえず要件を満たせるコンポーネントを作ってみます。

※**注意:** コメントは見やすさを優先して、すべて `//` で統一していますが、このままでは動かない箇所がある可能性があります。

```tsx
import { CheckIcon } from "@chakra-ui/icons";
import { Button, ButtonProps } from "@chakra-ui/react";

export interface SaveButtonProps extends ButtonProps {} // (1)

const SaveButton: React.FC<SaveButtonProps> = (props) => {
  const { children, ...buttonProps } = props; // (2)
  return (
    <Button
      colorScheme="green" // (3)
      leftIcon={<CheckIcon />} // (4)
      {...buttonProps} // (5)
    >
      {children} // (6)
    </Button>
  );
};

export default SaveButton;
```

| No | 説明 |
| --- | --- |
| **(1)** | コンポーネント用の Props インターフェースを、ベースとなるコンポーネントの Props 「`ButtonProps`」 を継承して定義する。 |
| **(2)** | ボタンのテキスト `children` とそれ以外の Props (ButtonProps の中身) で分ける。 |
| **(3)** | ボタンの色を緑色にする。 |
| **(4)** | ボタンの左側にチェックアイコンをつける。(アイコンは Chakra UI が提供する `@chakra-ui/icons` を利用。) |
| **(5)** | ベースコンポーネントの Props をすべて渡す。 |
| **(6)** | ボタンのテキストを指定する。 |

使い方は次のとおりです。

```jsx
import SaveButton from "./components/SaveButton";

// 中略
<SaveButton>Save</SaveButton>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8552/c6ed62aa-4929-4c6c-c6df-79b25e50ed1f.png)

このように、ボタンの文字を指定するだけでいい保存ボタンコンポーネントができました。

DevTools などで見てみると、この時点でマージンは指定されていません。

このコンポーネントの Props は、`ButtonProps` を継承して定義しているため、`Button` を拡張するようにデザインを変更することができます。

```jsx
// 横幅を親要素いっぱいに広げる
<SaveButton w="full">ほぞーーーん</SaveButton>

// 角張ったボタンにする
<SaveButton borderRadius={0}>SAVE</SaveButton>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8552/191e7574-d9d7-681e-1308-884462eb8e00.png)

### マージンの指定について

Button には、マージンに関する Props を持っているため、コンポーネントを定義したあとでも自由にマージンを指定することができます。

```tsx
// 例) 縦並びは margin-top を設定
<SaveButton>あっちに保存</SaveButton>
<SaveButton mt={2}>こっちに保存</SaveButton>

// 例) 横並びは margin-left を設定
<SaveButton>そっちに保存</SaveButton>
<SaveButton ml={2}>どっちに保存？</SaveButton>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8552/ca8a4ca8-465d-94db-2db4-07f1959f154b.png)


### 制限をつけた実装

マージン関連の話題からそれますが・・・。

Button のもつ Props を使えるため、柔軟である一方、ボタンの色やアイコンも上書きすることができます。

```jsx
// 赤い色で、ワーニングのアイコン
<SaveButton colorScheme="red" leftIcon={<WarningIcon />}>Save?</SaveButton>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8552/acd1e54c-17c8-446f-73a0-3f1179d0b345.png)

これだとコンポーネントを用意した意味がありません。

ということで、必要な機能に絞ったコンポーネントを作ってみます。

```tsx
import { CheckIcon } from "@chakra-ui/icons";
import { Button, ButtonProps } from "@chakra-ui/react";

export interface SaveButtonProps {
  children: string; // (1)
  onClick?: React.MouseEventHandler<HTMLButtonElement>; // (2)
  buttonPropsEx?: ButtonProps; // (3)
}

const SaveButton: React.FC<SaveButtonProps> = ({
  children,
  onClick = () => {},
  buttonPropsEx = {}
}) => {
  return (
    <Button
      colorScheme="green"
      leftIcon={<CheckIcon />}
      onClick={onClick}
      {...buttonPropsEx} // (4)
    >
      {children}
    </Button>
  );
};

export default SaveButton;
```

| No | 説明 |
| --- | --- |
| **(1)** | ボタンのテキストを指定できるようにする。 |
| **(2)** | ボタンクリック時のイベントを指定できるようにする。 |
| **(3)** | どうしてもボタンを拡張したくなった場合の拡張ポイント。 |
| **(4)** | 緊急ハッチをすべて展開する。 |

制限をかけるとしても、どうしても拡張が必要な場面が出てくるかもしれません。
その場合に備えて、ButtonProps を直接指定する `buttonPropsEx` という Props を用意しました。

こうすることで、柔軟性をもたせつつ、ある程度制限をかけたコンポーネントを作ることができます。
もちろん、TypeScript の型も効いています。

また、この例のように `xxPropsEx` という名前にすることで、拡張している箇所を `PropsEx` で検索することができるため、必要以上に拡張されていないかを調査することもできます。

```tsx
// こらこら・・・！
<SaveButton
  buttonPropsEx={{
    colorScheme: "red",
    leftIcon: <WarningIcon />
  }}
>
  Delete
</SaveButton>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8552/b32c4293-8bb2-0ded-e4c2-c06d00300017.png)


## Case 2. ヘッダー

次に、ページのヘッダーを作ってみようと思います。
Atomic Design では Organisms 相当でしょうか・・・。

コンポーネントの要件は次のとおりとします。

- 左側にプロダクトアイコンを表示する。(今回は簡略化するため、アイコンを表示する。)
- アイコンの右にメニューを表示する。
- ヘッダーの右に、ログインユーザーの情報を表示する。

よくあるヘッダーですね。

### 実装

Organisms レベルのコンポーネントを作るときは、基本的に `<Box>` で囲ってしまってもいいでしょう。

```tsx
import { StarIcon } from "@chakra-ui/icons";
import { Box, BoxProps, Heading, HStack, Spacer, Text } from "@chakra-ui/react";

export interface PageHeaderProps extends BoxProps {} // (1)

const PageHeader: React.VFC<PageHeaderProps> = ({ ...boxProps }) => {
  return (
    <Box px={6} py={4} borderWidth="1px" {...boxProps}> // (2)
      <HStack spacing={4}>
        <Heading size="md">
          <StarIcon />
        </Heading>
        <Text>メニュー 1</Text>
        <Text>メニュー 2</Text>
        <Text>メニュー 3</Text>
        <Spacer /> // (3)
        <Text>あなた 様</Text>
      </HStack>
    </Box>
  );
};

export default PageHeader;
```

| No | 説明 |
| --- | --- |
| **(1)** | BoxProps を拡張した Props インターフェースを定義する。 |
| **(2)** | Props をすべて渡す。 |
| **(3)** | Chakra UI にて、中間を開けることができるコンポーネント。 |

Case 1. のシンプルな実装で説明したとおり、BoxProps を継承した Props を採用することで、Box に指定できるものは何でも指定することができます。

### マージンの指定

コンポーネントを配置したあとで、**必要に応じてマージンなどを指定することができる** ようになります。

```tsx
import PageHeader from "./components/PageHeader";

// ページヘッダーを、画面上部からちょっと離す。
<PageHeader w="100vw" mt={4} />
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8552/bc83170a-7b06-9f61-346c-ac2161070c20.png)



## まとめ

Chakra UI を使用して、コンポーネントをいくつか定義してみました。

Chakra UI の Style に関する Props を使用することで、コンポーネント自体にマージンなどを定義しなくても、あとから ~~どうにでも~~ 柔軟にレイアウトができることが分かりました。

Chakra UI には、今回紹介した Box の他にも、中央揃えする `Center` や、縦並び、横並びにする `Stack`, `VStack`, `HStack`、複雑なレイアウトを実装できる `Flex`, `Grid`, `SimpleGrid` など、レイアウトに関するコンポーネントが多く用意されており、**大変魅力的** です！

・・・うっかり **Chakra UI はいいぞ** みたいな記事になりかけてしまいましたが、Chakra UI はユーティリティベースで **型安全な Tailwind CSS** として使えると思うので、まだ触ったことのない方はぜひ試してみてはいかがでしょうか。

明日 12/2 は、 [@denkiuo604](https://qiita.com/denkiuo604) さんの [MUI (Material-UI) で簡単なクイズアプリを作った話](https://qiita.com/denkiuo604/items/98d16d3c856e797b7cfb) です。




## 付録: CodeSandbox に React + TypeScript + Chakra UI の環境を作ったときのメモ

(できあがったテンプレートが [こちら (CodeSandbox)](https://codesandbox.io/s/chakra-ui-sandbox-l1llz?file=/src/App.tsx) です。Fork してお使いください。)

1. CodeSandbox の「Create Sandbox」をクリックし、テンプレート `React TypeScript` をクリックする。

2. Dependencies に、次の 4 つと、アイコン関連のライブラリーを追加する。(2021 年 12 月 1 日時点の [Chakra UI 公式サイト](https://chakra-ui.com/docs/getting-started#installation) より)
   - `@chakra-ui/react` -> `1.7.2`
   - `@emotion/react@^11` -> `11.7.0`
   - `@emotion/styled@^11` -> `11.6.0`
   - `framer-motion@^4` -> `5.3.3`
   - `@chakra-ui/icons` -> `1.1.1`

3. `App.tsx` を次のとおり変更する。

```tsx
import {
  Box,
  Center,
  ChakraProvider,
  Heading,
  Text,
  VStack
} from "@chakra-ui/react";
import "./styles.css";

export default function App() {
  return (
    <ChakraProvider>
      <Box> {/* 外枠 */}
        <Center> {/* 子要素を中央に配置する */}
          <VStack> {/* 子要素を縦に並べる */}
            <Heading>Hello CodeSandbox</Heading>
            <Text>Start editing to see some magic happen!</Text>
          </VStack>
        </Center>
      </Box>
    </ChakraProvider>
  );
}
   ```