【Vite × React × TypeScript】Chakra UI v3でボタンのローディング切り替えの実装方法

# はじめに

お疲れ様です。りつです。

現在以下のUdemyの講座を学習中です。

https://www.udemy.com/course/react_stepup/?couponCode=STEPUP20241201

今回は、Chakra UI v3のボタンに関するtipsをご紹介します。

# 問題

参考動画の中では、ボタンの実装を以下のように行っていました。

`isLoading`属性を指定することで、ボタンのローディングのON/OFFを切り替えられるとのことでしたが、こちらの動画では`@chakra-ui/react": "1.2.1`が使用されていたため、Chakra UIのv3ではうまく動きませんでした。

```react:src/components/atoms/button/PrimaryButton.tsx
import { Button } from "@chakra-ui/react";

/** 省略 **/

<Button
  bg="teal.400"
  color="white"
  _hover={{ opacity: 0.8 }}
  disabled={disabled}
  isLoading={loading}
  onClick={onClick}>
    { children }
</Button>

/** 省略 **/
```

# 解決方法

解決にあたって、注意する点が2点あります。
1. `Button`のインポートは`@chakra-ui/react`ではなく、`@/components/ui/button`から行う
2. `isLoading`属性ではなく、`loading`属性を使用する

## 1. `Button`のインポートは`@chakra-ui/react`ではなく、`@/components/ui/button`から行う

まず1つ目の注意点ですが、`Button`コンポーネントのインポート元が`@chakra-ui/react`の場合、後述する`loading`属性が使えません。

公式サイトの以下のページを元にChakra UI v3のインストールを行った場合、`src/components/ui`ディレクトリ配下に`button.tsx`というファイルが自動生成されているはずですので、そちらから`Button`コンポーネントをインポートするようにしましょう。

https://www.chakra-ui.com/docs/get-started/frameworks/vite

もし`src/components/ui/button.tsx`が存在しない場合は、以下のコマンドを実行してください。

```terminal
npx @chakra-ui/cli snippet add button
```

インポート元を修正した場合、ソースコードは以下のようになります。

```diff_jsx:src/components/atoms/button/PrimaryButton.tsx
- import { Button } from "@chakra-ui/react";
+ import { Button } from "@/components/ui/button";

/** 省略 **/

<Button
  bg="teal.400"
  color="white"
  _hover={{ opacity: 0.8 }}
  disabled={disabled}
  isLoading={loading}
  onClick={onClick}>
    { children }
</Button>

/** 省略 **/
```

:::note info
自分の場合は上記のインストール手順だけではエラーになってしまったため、追加で以下の記事の内容を実施しています。
同じエラーが発生する場合は参考にしてみてください。
:::

https://qiita.com/ritsu21ctws/items/8035044a11ab6b235ac0

## 2. `isLoading`属性ではなく、`loading`属性を使用する

Chakra UI v3の公式ドキュメントによると、ローディング中のボタンを表現するには、`loading`属性を追加してあげればよいとのことです。

> Use the `loading` and `loadingText` prop to show a loading spinner

https://www.chakra-ui.com/docs/components/button#loading

試しに`loading`属性を付与すると、画面上はボタンがローディング状態になります。

```diff_tsx:src/components/atoms/button/PrimaryButton.tsx
- import { Button } from "@chakra-ui/react";
+ import { Button } from "@/components/ui/button";

/** 省略 **/

<Button
  bg="teal.400"
  color="white"
  _hover={{ opacity: 0.8 }}
  disabled={disabled}
- isLoading={loading}
+ loading
  onClick={onClick}>
    { children }
</Button>

/** 省略 **/
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/faee2b74-9c88-f9fe-2c94-f0343c24e000.png)

ここで、例えばbooleanで定義された`loading`という変数の値によって、ボタンのローディング状態を切り替えたい場合、以下のように記述できます。

```react:src/components/atoms/button/PrimaryButton.tsx
import React, { memo } from "react";
import { Button } from "@/components/ui/button"

type Props = {
  children: React.ReactNode;
  disabled?: boolean;
  loading?: boolean;
  onClick: () => void;
}

export const PrimaryButton: React.FC<Props> = memo((props) => {
  const { children, disabled = false, loading = false, onClick } = props;
  return (
    <Button
      bg="teal.400"
      color="white"
      _hover={{ opacity: 0.8 }}
      disabled={disabled}
      loading={loading}
      onClick={onClick}>
        { children }
    </Button>
  );
});
```

<details><summary>上記ボタンの呼び出し元（疑似ログイン機能）</summary>

```react:src/components/pages/Login.tsx
import React, { memo, useState } from "react";
import { Box, Flex, Heading, Input, Separator, Stack } from "@chakra-ui/react";
import { PrimaryButton } from "../atoms/button/PrimaryButton";
import { useAuth } from "@/hooks/useAuth";

export const Login: React.FC = memo(() => {
  const { login, loading } = useAuth();
  const [userId, setUserId] = useState('');

  const onChangeUserId = (e: React.ChangeEvent<HTMLInputElement>) => setUserId(e.target.value);

  const onClickLogin = () => login(userId);

  return (
    <Flex align="center" justify="center" height="100vh">
      <Box bg="white" w="sm" p={4} borderRadius="md" shadow="md">
        <Heading as="h1" size="lg" textAlign="center">ユーザー管理アプリ</Heading>
        <Separator my={4} />
        <Stack gap={6} py={4} px={10}>
          <Input placeholder="ユーザーID" value={userId} onChange={onChangeUserId}/>
          <PrimaryButton
            disabled={userId === ''}
            loading={loading}
            onClick={onClickLogin}
          >
            ログイン
          </PrimaryButton>
        </Stack>
      </Box>
    </Flex>
  );
});
```
</details>

上記により、ログイン中はログインボタンをローディング中にし、ログイン成功後はローディングを解除する、といった挙動が実現できます。

# おわりに

自分は最初、`Button`コンポーネントを`@chakra-ui/react`からインポートしていたため、公式ドキュメントに記載されているはずの`loading`属性がエラーとなってしまったので、少し困惑しました。

ボタンのローディング切り替えは便利な機能なので、今後の個人開発などでも活用していければと思います。

# 参考

https://www.udemy.com/course/react_stepup/?couponCode=STEPUP20241201

https://www.chakra-ui.com/docs/get-started/frameworks/vite

https://qiita.com/ritsu21ctws/items/8035044a11ab6b235ac0

https://www.chakra-ui.com/docs/components/button#loading