こんにちは、0yuといいます。普段は某エンタメ企業でフロントエンドエンジニアをしている傍ら、株式会社ANYLANDのファンサービス事業でフロントエンド業務のお手伝いをさせていただいています。
はじめに
株式会社ANYLANDでは、ファンクラブサイトのフロントエンド開発においてUIライブラリに「Chakra UI」を使用しています。
先日、私たちはChakra UIのバージョンv2からv3へのメジャーアップデート対応を行いました。このアップデートには多数の破壊的変更が含まれており、詳細は公式の「Migration to v3」に記載されています。
今回のアップデート対応は、QA検証を含めて約3ヶ月の期間を要し、ファイル変更数259件に及ぶ大規模な改修となりました。
Chakra UIのv2→v3アップデートに関する事例記事などはまだ世間的にあまり多くないように見受けられたため、本記事が今後、同様のアップデート対応に取り組まれる方々の一助となれば幸いです。
また、本記事をご覧になってもし誤りや気づいた点などがございましたら、ぜひご意見をいただけますと幸いです。
アップデートの背景
Chakra UIは内部的にランタイム CSS-in-JSであるEmotionを利用しているため、v3でもReact Server Componentsには変わらず非対応です。
これは、将来的なApp RouterのServer Componentへの完全対応を見据える上での課題でした。他ライブラリへの移行も検討しましたが、UIコンポーネントの全面的な書き換えを行う場合、移行先のライブラリ選定、移行戦略の策定、そして将来的な保守性やエンジニアのキャッチアップコストまで考慮に入れると、v3へのアップデート以上の膨大な工数がかかることが予測されました。
一方、システムのコアとなるChakra UIが旧バージョンのまま長期間放置された場合、今後の開発スピードに支障をきたす可能性があります。Server Componentsへの対応は引き続き検討課題ではありますが、まずは現在の開発環境の健全性を維持し、チームの生産性を確保することが最優先であると判断し、私たちはv3 へのアップデートを実施しました。
アップデートの手順
基本的には公式のマイグレーションドキュメントに沿って進めますが、マイグレーションドキュメントには直接記載がないものの対応必須な破壊的変更でハマったポイントなどもあったため、そちらも併せてご紹介します。
依存パッケージの更新
npm uninstall @emotion/styled framer-motion
npm install @chakra-ui/react@latest @emotion/react@latest
npx @chakra-ui/cli snippet add
主な変更点:
-
@chakra-ui/icons
→ 非推奨になったため、代わりにreact-icons
を使用します -
framer-motion
→ アニメーションライブラリとして使用していたframer-motion
への依存は削除されました -
styleConfig
,multiStyleConfig
→recipes
に置き換えます
カスタムテーマ設定の移行とChakraProviderの書き換え
v3ではuseTheme
の使用方法が変更されました。v2では、extendTheme
でテーマオブジェクトを直接取得していましたが、v3ではdefineConfig
を使ってテーマを取得・管理します。
import { createSystem, defaultConfig } from "@chakra-ui/react"
export const customConfig = defineConfig({
theme: {
tokens: {
fonts: {
heading: { value: `'Figtree', sans-serif` },
body: { value: `'Figtree', sans-serif` },
},
},
},
})
またカラーなどのすべてのテーマトークンの値は、以下のようにkeyとvalueを持つオブジェクトにラップする必要があるため、useThemeColors.ts
を書き換えます。テーマトークンの詳細については、こちらを参照してください。
最終的にChakraProvider
のvalue
プロパティにuseTheme()
フックで取得したcustomConfig
を渡すことで、テーマを設定するように変更します。
Before(v2 の例):
import { ChakraProvider } from "@chakra-ui/react"
const App: FCAppPropsAppInitialProps>> = ({ Component, pageProps }) => {
const theme = useTheme()
return (
ChakraProvider theme={theme}>
Component {...pageProps} />
ChakraProvider>
)
}
export default App
After(v3 の例):
"use client"
import { ChakraProvider } from "@chakra-ui/react"
import { ThemeProvider } from "next-themes"
import { ColorModeProvider } from "./color-mode"
import { useTheme } from "../../libs/chakra-ui/hooks/useTheme"
const App: FCAppPropsAppInitialProps>> = ({ Component, pageProps }) => {
const customConfig = useTheme()
return (
ChakraProvider value={customConfig}>
Component {...pageProps} />
ChakraProvider>
)
}
ハマりどころ① 〜 ColorModeProvider の導入でカラーテーマが崩れる〜
Chakra UI v3では、ダークモード対応にnext-themesを採用しています。
ColorModeProvider: composes the next-themes provider component
マイグレーションのドキュメントには、従来のChakraProvider
だけではなく ColorModeProvider
も追加するように書かれています。このColorModeProvider
は、@chakra-ui/cli
で追加されたスニペッドであり、実質的に next-themes
の ThemeProvider
をラップしたものでした。ThemeProvider
をオプションなしで導入すると、ダークモード時にページ全体のテキストカラーを白、背景色を黒に切り替えるCSSが追加されます。
具体的に、next-themes 単体の挙動として、@media (prefers-color-scheme: dark)
を使ってダークモード時に:root
擬似クラスで設定しているテキストと背景色に使用するカスタムプロパティを書き換えます。
bodyタグではカスタムプロパティを使ってテキストと背景色を指定しているので、上記の書き換えによって色が切り替わるという仕組みになっています。
Chakra UI + next-themes を組み合わせたときの挙動としては、ダークモード時にタグではなく
タグに指定してあるカスタムプロパティの値を書き換えます。
結論として、私達のプロジェクトではダークモードを使用していなかったため、この仕様によってデフォルトの色の設定が上書きされ、テキストの色が設定されていなかった箇所がすべて白色になってしまいました。さらに、背景色は白ベースに設定していたため、文字が背景と同化して見えなくなるという問題が発生しました。
この問題に対して、私たちはColorModeProvider
を外すことで対応しました。
Dark Modeのドキュメントsetupにcolor-mode
スニペットとして追加するように記載があるため、オプショナルな機能なので追加しなくても問題ないという判断をしました。
ハマりどころ② 〜tsconfig.json
のmoduleResolution
の変更 〜
v3からは、tsconfig.json
のmoduleResolution
に"node"
を使用していた場合”bundler”に変更する必要があります。
"moduleResolution": "node"
"moduleResolution": "bundler"
tsconfigのmoduleResolutionはモジュール解決の戦略を指定するオプションです。
変更前に指定しているnodeは、CommonJS時代の挙動を再現するモードのため現在は推奨されておらず、代わりにバンドラーの挙動を再現するbundlerが推奨されています。
Chakra UIにおいては、moduleResolution:"node"
を使用したままv3にアップデートした場合、propsの型が合わなくなります。
例えば、以下のようなCheckBoxのコンポーネントがあったときに、
return (
Checkbox.Root>
Checkbox.HiddenInput isChecked />
Checkbox.Control />
Checkbox.Label>{label}Checkbox.Label>
Checkbox.Root>
)
Chakra UIのv3ではCheckbox.HiddenInputに指定するpropsはisCheckedではなくcheckedに変わっています。しかし、型エラーが検出されることはありません。
これはmoduleResolution: "node"
のままだと、Chakra UIの内部で使用しているArk UIの型定義がany
になり、Propsの型の誤りに気づけないためです。
moduleResolutionをbundler
に変更することで、Chakra UI v3が依存するArk UIの型検査が正常に動作するようになります。
本対応についてはv3のInstallationドキュメントのUpdate tsconfigの項目に記載がありますが、マイグレーションドキュメントには記載がなく移行作業の中では気づきにくいため、ハマりどころとして記載しました。
主要なコンポーネントごとの変更点
Chakra UIはv3からshadcn/uiにインスパイアされた構成となり、より柔軟にコンポーネントをカスタマイズできるようになった反面、従来の使用方法から大きく変更されたコンポーネントが多く存在します。
Shadcn: For inspiring the CLI and driving the idea of copy-paste snippets which Chakra now embraces.
Announcing v3より
今回は、その中のいくつかのコンポーネントの変更についてまとめます。
Accordion
v2にあったAccordionコンポーネントは、以下のように Accordion.Root
を中心とした構成に書き換える必要があります。
Accordion.Root>
Accordion.Item>
Accordion.ItemTrigger>
Accordion.ItemIndicator />
Accordion.ItemTrigger>
Accordion.ItemContent>
Accordion.ItemBody />
Accordion.ItemContent>
Accordion.Item>
Accordion.Root>
AccordionButton などは通常のButton要素としておきます。
Accordion.Root spaceY="4" variant="plain" collapsible defaultValue={["b"]}>
{items.map((item, index) => (
Accordion.Item key={index} value={item.value}>
Box position="relative">
Accordion.ItemTrigger indicatorPlacement="start">
{item.title}
Accordion.ItemTrigger>
AbsoluteC.enter axis="vertical" insetEnd="0">
Button variant="subtle" colorPalette="blue">
Action
Button>
AbsoluteCenter>
Box>
Accordion.ItemContent>{item.text}AccordionItemContent>
Accordion.Item>
))}
Accordion.Root>
また、AccordionIcon は廃止されたため 開閉の矢印ボタンは
を使用します。
AccordionIcon: A chevron-down icon that rotates based on the expanded/collapsed state
https://v2.chakra-ui.com/docs/components/accordion より
Before(v2 の例):
"use client"
import { Accordion, Span, Stack, Text } from "@chakra-ui/react"
import { useState } from "react"
const Demo = () => {
const [value, setValue] = useState(["second-item"])
return (
Expanded: {value.join(", ")}
setValue(e.value)}>
{items.map((item, index) => (
{item.title}
-
{item.text}
))}
)
}
// ...
After(v3 の例):
"use client"
import { Accordion, Span, Stack, Text } from "@chakra-ui/react"
import { useState } from "react"
const Demo = () => {
const [value, setValue] = useState(["second-item"])
return (
Expanded: {value.join(", ")}
setValue(e.value)}>
{items.map((item, index) => (
{item.title}
+
{item.text}
))}
)
}
// ...
Modal
主な変更点は以下になります。
-
Modal
は廃止され、Dialog
コンポーネントに変更された -
isCentered
propsが廃止され、代わりにplacement="center"
props を使用する -
isOpen
およびonClose
propsが廃止され、代わりにopen
およびonOpenChange
props を使用する
これまでChakra UI のModal
コンポーネントは、以下のように直接インポートして使用することができました。
import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
useDisclosure,
} from "@chakra-ui/react"
そして、useDisclosure
を使って開閉状態を管理しながら、以下のような形で Modal
を使用していました。
export const RestrictionAdder: FCProps> = ({ appendRestriction }) => {
const { isOpen, onOpen, onClose } = useDisclosure()
const handleOpenAdderModal = useCallback(() => {
onOpen()
}, [onOpen])
const handleAdderModalSubmit = useCallback(
(field: MagazineRestrictionFromFields) => {
appendRestriction(field)
onClose()
},
[appendRestriction, onClose],
)
return (
>
Button
onClick={handleOpenAdderModal}
borderRadius="lg"
colorScheme="gray"
fontWeight="nomal"
marginTop="12px">
条件を追加
Button>
Modal isOpen={isOpen} onClose={onClose} size="full">
ModalOverlay />
ModalContent padding="24px 40px">
ModalHeader>条件を追加ModalHeader>
ModalCloseButton />
ModalBody>
RestrictionForm
onSubmit={handleAdderModalSubmit}
defaultValues={{}}
/>
ModalBody>
ModalContent>
Modal>
>
)
}
v3からは、従来は Chakra UI 側が内部的に処理していたモーダルの表示位置(画面中央への配置)やオーバーレイ、モーダルの開閉処理などを、v3 以降では開発者自身が明示的に記述する設計に変更されました。そのため、モーダルの実装は以下のように Dialog.Root
を中心とした構成に書き換える必要があります。
import { Button, CloseButton, Dialog, Portal } from "@chakra-ui/react"
Dialog.Root size="full" padding="24px 40px">
Dialog.Trigger>
Button
width="full"
borderRadius="lg"
bgColor="gray.100"
color="gray.950"
fontSize="16px"
fontWeight="normal"
marginTop="12px">
条件を追加
Button>
Dialog.Trigger>
Portal>
Dialog.Positioner>
Dialog.Content>
Dialog.Header>条件を追加Dialog.Header>
Dialog.CloseTrigger>
CloseButton position="absolute" top="8px" right="8px" />
Dialog.CloseTrigger>
Dialog.Body>
RestrictionForm
onSubmit={handleAdderModalSubmit}
defaultValues={{}}
/>
Dialog.Body>
Dialog.Content>
Dialog.Positioner>
Portal>
Dialog.Root>
主な差分
項目 | Modal | Dialog |
---|---|---|
開閉制御 |
useDisclosure で状態管理(明示的に isOpen / onClose を渡す) |
Dialog.Trigger によって制御(状態管理は内部に隠蔽されている) |
ラッパー構成 |
Modal コンポーネント単体で完結 |
Dialog.Root + Portal + Dialog.Positioner + Dialog.Content で構成 |
閉じるボタン |
ModalCloseButton を配置するのみ |
Dialog.CloseTrigger でラップし、その中に CloseButton を明示的に配置する |
表示位置・構造 | Chakra UI が内部的に中央寄せ等のスタイルを付与 |
Dialog.Positioner を使って表示位置を明示的に制御する |
Stack
主な変更点は以下になります。
-
spacing
porps は廃止されgap
propsを使用するようになった -
StackItem
は廃止されBox
コンポーネントを直接使用するようになった -
StackDivider
→StackSeparator
に変更になった
Stack
spaceX={{ base: "0", lg: "6" }}
spaceY={{ base: "8", lg: "6" }}
separator={StackSeparator />}
padding="24px 0">
{children}
Stack>
Before(v2 の例):
Stack gap="4">
SimpleGrid
gap="4"
>
SimpleGrid>
// ...
Stack>
After(v3 の例):
Stack spacing="4">
SimpleGrid
spacing="4"
>
SimpleGrid>
// ...
Stack>
Image
主な変更点は以下になります。
- fallbackを使わずnativeの
img
要素をレンダリングするようになった -
fallbackSrc
が削除された -
useImage
hookが削除された -
Img
コンポーネントは廃止され、代わりにImage
コンポーネントを直接使用するようになった
NumberInput
主な変更点は以下になります。
-
NumberInputStepper
はNumberInput.Control
に変更された -
NumberInputStepperIncrement
はNumberInput.IncrementTrigger
に変更された -
NumberInputStepperDecrement
はNumberInput.DecrementTrigger
に変更された -
onChange
propsはonValueChange
に変更された
またv2では、NumberInputFieldのフォーカス時やエラー時のカラーテーマをfocusBorderColor
やerrorBorderColor
のようなstyleプロパティに指定して変更を行うことが可能でした。
ですがv3からはこれらのプロパティは削除され、代わりにbaseスタイル内でCSS変数を使ってボーダー色を決めるように変更されています。
Before(v2 の例):
NumberInput>
NumberInputField />
NumberInputStepper>
NumberIncrementStepper />
NumberDecrementStepper />
NumberInputStepper>
NumberInput>
After(v3 の例):
NumberInput.Root>
NumberInput.Input />
NumberInput.Control>
NumberInput.IncrementTrigger />
NumberInput.DecrementTrigger />
NumberInput.Control>
NumberInput.Root>
IconButton
主な変更点は以下になります。
-
icon
propsは削除されchildren
で直接アイコンを渡すようになった -
isRounded
は削除されborderRadius=full
propsを使うようになった
Before(v2 の例):
IconButton
icon={BiCheck />}
aria-label="Submit"
{...getSubmitButtonProps}
/>
After(v3 の例):
IconButton
size="sm"
bgColor="gray.100"
aria-label="Edit"
onClick={handleEdit}>
BiSearch color="black" />
IconButton>
Toast
v2までは import { useToast } from "@chakra-ui/react";
のようにuseToast
を直接インポートして使用することができましたが、v3では存在しないため、代わりにtoastコンポーネントを使用します。
toastコンポーネントを使用する際は、事前準備として_app.tsxなどに
を記載する必要があります。その上で、toaster
のsnippetを追加して使います。
npx @chakra-ui/cli snippet add toaster
import { toaster } from "../../../../components/ui/toaster"
toaster.create({ title: "新しい記事を作成しました", status: "success" })
最後に
ここまでに記載した通り、v3へのマイグレーションのドキュメントには記載されていない項目がいくつもありました。
本記事では、私たちのプロジェクトで使用していなかったコンポーネントについては触れていませんが、他にも同様にドキュメントに記載されていない破壊的変更がいくつか存在します。
このような予期せぬ変更への対応が必要だったため、今回の移行作業は当初の想定よりも大幅に時間を要しました。これからChakra UI v3へのアップデートを検討されている方は、作業工数を多めに見積もって着手されることを強くおすすめします。
株式会社ANYLANDでは、一緒に働く仲間を募集しています!
株式会社ANYLANDは、ファンサービス事業を中心に、複数のサービスを開発・提供しているエンタメTech企業です。
少数精鋭な環境下で大きな裁量を持って働きたいという方、会社/事業と一緒に成長していきたい方を募集しています!
Views: 0