Chakra UI の useBreakpointValue()
を使って Button コンポーネントの size をレスポンシブで変更できるようにしました。
useBreakpointValue()
が保持した値を返すので、ボタンのラベルも合わせて保持できるのでは?と Next.js のアプリで試していて罠にハマったのでメモ。
環境
@chakra-ui/icons: ^1.0.13 @chakra-ui/react: ^1.6.3 next: ^10.0.0
複数の値を useBreakpointValue()
で保持する
import { Button, useBreakpointValue } from '@chakra-ui/react'; const ResponsiveButton: VFC = () => { const buttonSize = useBreakpointValue(['sm', 'md']); const buttonLabel = useBreakpointValue(['MyButton', 'MyResponsiveButton']); return ( <Button type="button" size={buttonSize}> {buttonLabel} </Button> ); }
レスポンシブで変更する値それぞれに useBreakpointValue()
を使えば当然複数の値をレスポンシブで変更できます。
ただ値が多くなると全ての値で hooks を呼び出すのはパフォーマンス的にもあまりよろしくないと思います。
今回は buttonSize
と buttonLabel
を同じブレイクポイントで変更したいので、これをセットにして useBreakpointValue()
に渡せば合理的だと考えました。
useBreakpointValue の型は次のように定義されています
// chakra-ui/packages/media-query/src/use-breakpoint-value.ts export function useBreakpointValue<T = any>( values: Record<string, T> | T[], defaultBreakpoint?: string, ): T | undefined
cf. chakra-ui/packages/media-query/src/use-breakpoint-value.ts | GitHub
保持される値が T = any
なので配列・オブジェクトで保持する事ができそうです。
🙅 Next.js で useBreakpointValue() に保持させるデータを配列で渡すとエラーになった
変数は分割代入で取れるので const [buttonSize, buttonLabel] = [size, label]
の形にしようと思いました。
const ResponsiveButton: VFC = () => { const [buttonSize, buttonLabel] = useBreakpointValue([ ['sm', '登録'], ['md', '登録する'], ]); return ( <Button type="button" size={buttonSize}> {buttonLabel} </Button> ); }
うまくいきそうです。
しかし実際には次のようなエラーが発生してしまいました。
=> Uncaught TypeError: Invalid attempt to destructure non-iterable instance. In order to be iterable, non-array objects must have a [Symbol.iterator]() method.
🙅 Object 形式でも同様
export const ResponsiveButton: VFC = () => { const { buttonSize, buttonLabel } = useBreakpointValue([ { buttonSize: 'sm', buttonLabel: '登録' }, { buttonSize: 'md', buttonLabel: '登録する' }, ]); // 略 }
=> TypeError: Cannot read property 'buttonSize' of undefined
Next.js だと初回レンダリング時に useBreakpointValue() が undefined
を返すのがエラーの原因
最初複数の値をセットで保存できないのかと思ったのですが、元の文字列の値を保持する状態に戻して console.log
で確認すると次のようになっていました
const ResponsiveButton: VFC = () => { const buttonSize = useBreakpointValue(['sm', 'md']); console.log({ buttonSize }); // 略 }
👇
1. { buttonSize: undefined } 2. { buttonSize: "sm" }
初回レンダリング時に useBreakpointValue()
が返す値が undefined
になっていました。なので先のコードでは配列形式のデータを想定して分割代入で変数に値を代入しようとしていたので初回レンダリング時に次のような処理になりエラーが発生してしまったようです
const [buttonSize, buttonLabel] = undefined; // => Uncaught TypeError: undefined is not iterable const {buttonSize, buttonLabel} = undefined; // => TypeError: Cannot destructure property 'buttonSize' of 'undefined' as it is undefined.
useBreakpointValue の実装を見てみる
useBreakpointValue
の実装を見てみると、useBreakpoint
で breakpoint を取得していて、これが Falsy だと undefined
を返すようになっており、breakpoint の取得は useBreakpoint
の useEffect
内で window.matchMedia
を使って取得するようになっていました。
export function useBreakpointValue<T = any>( values: Record<string, T> | T[], defaultBreakpoint?: string, ): T | undefined { const breakpoint = useBreakpoint(defaultBreakpoint) const theme = useTheme() if (!breakpoint) return undefined // ...
cf. chakra-ui/packages/media-query/src/use-breakpoint-value.ts | GitHub
つまり yarn dev
で実行している時の初回レンダリングは useEffect 前の値が一度返されていて、この時初期値の undefined
が返されているようです。 Next.js では サーバーサイドの処理も同じコンポーネントで行われるので、クライアントサイドでのみ実行される useEffect
内でないと window
オブジェクトが存在しないので matchMedia
が使えず breakpoint が取得できないのでこのような仕様になっているのだと思います。
⚠️ defaultBreakpoint オプションが効かない 追記: @chakra-ui/react
v1.6.4 より古いの場合
実装には defaultBreakpoint
の指定ができるようになっており、この値がデフォルトのブレイクポイントとして使われるっぽいのですが、指定してみたのですが type error になり type error を ignore してもデフォルトの値は返されず undefined
が返され上手く動作しませんでした。
import { useBreakpointValue } from '@chakra-ui/react'; export const ResponsiveButton: VFC = () => { const { buttonSize, label } = useBreakpointValue( { sm: { buttonSize: 'sm', buttonLabel: '登録' }, md: { buttonSize: 'md', buttonLabel: '登録する' }, }, // defaultBreakpoint 'sm', // => Expected 1 arguments, but got 2. ); // … 略 };
実装された PR は merge されているのですが、Chakra UI のドキュメント にも記載がないので Chakra UI @chakra-ui/react: ^1.6.3
ではまだ使えないオプションなのかもです。
追記: @chakra-ui/react@1.6.4 以降なら使えそう
16-06-2021
@chakra-ui/react@1.6.4
Media Queryv1.1.0
🚀useBreakpointValue()
now supports receiving adefaultBreakpoint
as the second argument to support SSR/SSG.
cf. chakra-ui/CHANGELOG.md | GitHub
2021 6/16 にリリースされた @chakra-ui/react v1.6.4
からは使えるっぽいです。
解決方法: 自前でデフォルト値を設定する
useBreakpointValue
の defaultBreakpoint
オプションが使えず、初期レンダリング時に undefined
が返ってくるので、useBreakpointValue() || defaultValue
として自前でデフォルト値を設定してしまえば分割代入を使っていてもエラーになりません。
Array パターン
import { useBreakpointValue } from '@chakra-ui/react'; type ButtonSizeType = string; type ButtonLabelType = string; type BreakpointType = [ButtonSizeType, ButtonLabelType]; const ResponsiveButton: VFC = () => { const responsiveValues: BreakpointType[] = [ ['sm', '登録'], ['md', '登録する'], ]; const [buttonSize, buttonLabel] = useBreakpointValue(responsiveValues) || responsiveValues[0]; }
初回レンダリング時は undefined || responsiveValues[0]
となり responsiveValues[0]
の値が使われるようになります。
Object パターン
import { useBreakpointValue } from '@chakra-ui/react'; import { BaseBreakpointConfig } from '@chakra-ui/theme-tools'; type ResponsiveValue = { buttonSize: string; buttonLabel: string; }; type BreakpointTypes = { [key in keyof BaseBreakpointConfig]?: ResponsiveValue; }; const ResponsiveButton = () => { const breakpointValues: BreakpointTypes = { sm: { buttonSize: 'sm', buttonLabel: '登録' }, md: { buttonSize: 'md', buttonLabel: '登録する' }, }; const defaultValue = breakpointValues.sm; const { buttonSize, buttonLabel } = useBreakpointValue(breakpointValues) || defaultValue;
BreakpointTypes
の key が Partial になっているので、defaultValue
の指定が breakpointValues
オブジェクトに存在するキーに限定できてないですが、Next.js の初回ロード時は defaultValue
が使われるようにできました。
追記: まとめ
- デフォルトだと
useBreakpointValue()
が初回レンダリング時にundefined
を返すので、分割代入で値を受け取ろうとするとエラーになる - Chakra UI
v1.6.4
以降ならuseBreakpointValue
の第二引数 (defaultBreakpoint
) オプションで初期値になるブレイクポイントを指定する - Chakra UI が
v1.6.4
より古い場合はuseBreakpointValue() || defaultValue
で初期値を自前で指定してしまう
所管
GitHub のコードを見に行くとデフォルト値を指定できる defaultBreakpoint
オプションがあり、これを渡せばサクット解決☆(ゝω・)
と思ったのですが、オプションが使えなかったというヲチで ||
を使うハック的な方法で何とか解決できました。
バグ踏みの達人なので、Chakra UI まだまだハマりそうです…
今回は Next.js の環境だったからこうなったのですが、React でも SSR をする環境だと SSR 時は node.js で実行されるので window オブジェクトがなく同じ問題にハマるだろうな〜と思いメモを書くことにしました。
そういえば、以前のお仕事で JSX 内でメディアクエリを利用してコンポーネントの出し分けをするコンポーネントを作成していて、SSR の際にメディアクエリが取得できず、結局クライアントで再度 React 描画時にコンポーネントの表示変更行われ Layout Shift (CLS) が発生してしまう問題に悩まされたことを思い出しました。 (その記事を描こうと思ったまま書いていなかった…
[参考]
チャクラ宙返りのおもちゃ流石にもう出てこないか…