かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React × Chakra UI レスポンシブでサイズの変わるボタンを作りたい

モック作りに Chakra UI を採用してみました。

レスポンシブ時に大きさの変わる <Button> コンポーネントを作るのに少しハマってしまったのでメモ。

環境
@chakra-ui/icons: ^1.0.13
@chakra-ui/react: ^1.6.3

Chakra UI の Button コンポーネントでは size props に配列形式でレスポンシブ時のボタンサイズの設定ができない

width や margin や font-size の指定などは配列で指定することができました。

<Box fontSize={['xs', 'sm', 'md']}>
  Responsive font-size
</Box>

同じ様に Button コンポーネントの size を指定しようとすると型エラーが表示されました

<Button type="button" size={['sm', 'sm', 'md']}>
  My Button
</Button>
//  Type 'string[]' is not assignable to type '(string & {}) | "sm" | "md" | "lg" | "xs"'.
//  Type 'string[]' is not assignable to type '"xs"'.
//  (JSX attribute) ThemingProps<"Button">.size?: (string & {}) | "lg" | "md" | "sm" | "xs"

Button Props
size
Type: "sm" | "md" | "lg" | "xs"
Default: "md"
cf. Button - Chakra UI

Button コンポーネントsize porps の型は "sm" | "md" | "lg" | "xs" で配列のレスポンシブ指定はできないようです。

useBreakpointValue() を使って動的にレスポンシブ時の値を設定する

useBreakpointValue is a custom hook which returns the value for the current breakpoint from the provided responsive values object. This hook also responds to the window resizing and returning the appropriate value for the new window size.
cf. useBreakpointValue - Chakra UI

ブレイクポイントに合わせて予め渡しておいた値が返される hooks を使って動的に props を設定することができるようです。

import { useBreakpointValue } from '@chakra-ui/react';

const ResponsiveButton: VFC = () => {
  const buttonSize = useBreakpointValue(['sm', 'sm', 'md']);

  return (
    <Button type="button" size={buttonSize}>
      My Responsive Button
    </Button>
  );
};

buttonSize の値がブレイクポイントに合わせて変更され、Buttonコンポーネントのサイズをレスポンシブで変更できるようになりました!

note.

こちらで 2020 年に size props も配列・オブジェクト形式でのレスポンシブ指定ができるようにしようとする議論があり v2 で実装する ロードマップにも載ったようですが issue も PR (feat: allow responsive values for `variant` and `size` by dodas · Pull Request #3258 · chakra-ui/chakra-ui · GitHub) close されてしまっているので、当分はこの useBreakpointValue を使う方法でないとダメそうです。

感想

Chakra UI まだ新しいフレームワークですが、React で使いやすく特に機能的なものはサクット実装できて便利です。
一方、指定方法が違うのにドキュメントに書かれてなかったり、同じ sm でもコンポーネントや props によってサイズが異なったり、元の HTML に存在する attribute と同じ名前だけど指定方法が異なる props のオプションはあったりなどハマる部分が結構あり、困りごとが少し調べにくいな〜 (時間がかかる) という印象です。
フレームワークなので慣れの問題でもありますが、まだバージョンも低いので仕様も変わっていくのかなと思っています。


[参照]


この記事を書くために改めて調べていてスポンシブ時の指定方法、配列とオブジェクトで指定する方法があるのに気づきました。

import { createBreakpoints } from "@chakra-ui/theme-tools"
// This is the default breakpoint
const breakpoints = createBreakpoints({
  sm: "30em",
  md: "48em",
  lg: "62em",
  xl: "80em",
  "2xl": "96em",
})
// Internally, we transform to this
const breakpoints = ["0em", "30em", "48em", "62em", "80em", "96em"]

Examples

<>
  <Box
    height={{
      base: "100%", // 0-48em
      md: "50%", // 48em-80em,
      xl: "25%", // 80em+
    }}
    bg="teal.400"
    width={[
      "100%", // 0-30em
      "50%", // 30em-48em
      "25%", // 48em-62em
      "15%", // 62em+
    ]}
  />
</>

cf. Responsive Styles - Chakra UI

つまり

  • Chakra UI の Breakpoint 指定は全て min-width になっている
  • 配列の指定方法は ['0 < xm', 'xs < sm', 'sm < md', 'md < lg', 'lg < 2xl', ' 2xl =< ']
  • 0 < md までと sm =< のような指定は 配列で ["100%", "100%", "50%"] と書かなくても {base: '100%', md: '50%'} と書くことができる

ドキュメントはしっかり読もう… (今回の反省)

オタク今の推し作品をサムネイルに設定しがち…

これは違うチャクラ…

TypeScript interface のプロパティの型を取得したい!

オブジェクトがリストになっているデータから、オブジェクト内の id をキーにした Map を作成したい時、id の型を interface から取得したかったのメモ

interface

interface Iidol {
  id: number;
  name: string;
  tyep: 'cute' | 'cool' | 'sexy' | 'pop';
}

1. interface のプロパティを直接指定して型を作ることができる

type IdolIdType = Iidol['id'];
// => type IdolIdType = number

2. プロパティ名のリテラル型を作成して、それを interface のプロパティとして型を作成

type IdolId = 'id';
type IdolIdType = Iidol[IdolId];
// => type IdolIdType = number

上記の方法で interface から type を作成することができました。
ただ個人的には interface からプロパティの型を取得する必要が出てしまうのであれば、最初からそのプロパティの型を定義しておくほうが見通しが良さそうと感じました。

type IdolIdType = number;

interface Iidol {
  id: IdolIdType;
  name: string;
  tyep: 'cute' | 'cool' | 'sexy' | 'pop';
}

IdolId をキーにした Map を作成する

type Idols = Iidol[];

このデータを Map にする

const idols: Iidol[] = [
  { id: 1, name: '星宮いちご', type: 'cute'},
  { id: 2, name: '霧矢あおい', type: 'cool'},
  // ...
];

const getIdoMap = (): ReadonlyMap<IdolIdType, Iidol> => {
  const map = new Map();
  idols.forEach((user) => {
    map.set(user['id'], user);
  });

  return map;
}

getIdoMap();

いい感じになりましたが getIdoMapidols が外にあって副作用があるし key が固定なので、もう少し抽象化してみます。

const getMapByKey =
  <T, K extends keyof T>(data: T[]) =>
  (key: K): ReadonlyMap<T[K], T> => {
    const map = new Map();
    idols.forEach((item) => {
      map.set(item[key], item);
    });

    return map;
  };

getMapByKey(idols) => ('id');

API から渡される オブジェクトの入った配列からキーを指定して Map が作れるようになりました!
interafce から型作らなくてもよかった…

所管

interface をオブジェクトのように扱うことでプロパティの型を作成することができる (Interface['property name'] )
ただ特定のプロパティの型が外部で必要であれば別途定義しておくほうが見通しが良い。
特定のプロパティの型が無くても成立する方法がないか考えてみるのが良い。

こんな感じでしょうか。
TypeScript のジェネリクス何となく理解してきました。T とか K って何なん?って思っていたのですが、抽象的な型を表すのに伝統的に使われているだけだったと知ってから苦手意識がなくなりました。 ループの i とか j みたいなものだったのか〜


[参考]

BADON 新刊出てた!!

React Warning: Cannot update a component (`Componet`) while rendering a different component.

Next.js (React) でログインユーザー情報がなければ / にリダイレクトするコンポーネントを作っていて、リロード時に state が無くなるので SessionStorage に保存しておいたデータを取ってきてログインを確認するような事をしていたら下記のような Warning が表示されました。
Warning: Cannot update a component (`Componet`) while rendering a different component.

結論: コンポーネントレンダリング中に別コンポーネントの state を変更しようとすると Warning が発生する。

⚠ 問題のあったコード

/pages/_app.tsx

import { AppProps } from 'next/app';
import { RecoilRoot } from 'recoil';
import { Auth } from '../Component/Auth';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <RecoilRoot>
      <Auth>
        <Component {...pageProps} />
      </Auth>
    </RecoilRoot>
  );
};

/hooks/useUser.tsx

import { useRecoilState } from 'recoil';

const readUser = (): UserInterface | undefined => {
  const user = sessionStorage.getItem('user');
  return user ? (JSON.parse(user) as UserInterface) : undefined;
};

interface IuseUser {
  user: UserInterface;
  isUser: () => boolean;
}

export const useUser = (): IuseUser => {
  const [user, setUserData] = useRecoilState(userState);

  const isUser = () => {
    if (user) {
      return true;
    }
    
    // SessionStorage にデータがあれば user state を更新する
    const userData = readUser();
    if (!userData) {
      return false;
    }

    setUserData(userData);
    return true;
  }

  return {
    user,
    isUser
  }
};

/Component/Auth.tsx

import { useEffect, useState, VFC } from 'react';
import { useRouter } from 'next/router';
import { useUser } from '../hooks/useUser';

interface AuthProps {
  children: React.ReactNode;
}

export const Auth: VFC<AuthProps> ({ children }) => {
  const [loading, setLoading] = useState<boolean>(true);
  const router = useRouter();
  const { isUser } = useUser();
  
  useEffect(() => {
    if (router.isReady) {
      setLoading(false);
    }
  }, [router.isReady]);
  
  if (loading) {
    return <Loading />
  }

  // state にも SessionStorage にもデータがない場合はリダイレクト
  if (!isUser()) {
    router.push('/');
    // リダイレクト前に children が一瞬表示されてしまわないように null を返す
    return null;
  }

  return <>{children}</>;
}

一度ログインして SessionStorage にデータが有る状態でリロードすると Warning が発生します。
Warning: Cannot update a component (`Componet`) while rendering a different component.

コンポーネントレンダリング中に別コンポーネントの state を変更しようとすると Warning が発生する。

レンダー中に setState を呼び出すことはサポートされていますが同じコンポーネントに対してのみ可能です。別のコンポーネントのレンダー中に setState を呼び出すと、警告が表示されるようになりました。

Warning: Cannot update a component from inside the function body of a different component.
この警告は、意図しない状態変更によって引き起こされるアプリケーションのバグを見つけるのに役立ちます。レンダーの結果として他のコンポーネントの状態を意図的に変更したいという稀なケースでは、setState 呼び出しを useEffect にラップすることができます。
cf. レンダー中のいくつかの更新に関する警告 | React v16.13.0 – React Blog

問題の箇所

/Component/Auth.tsx

export const Auth: VFC<AuthProps> ({ children }) => {
  // 略

  // state にも SessionStorage にもデータがない場合はリダイレクト
  if (!isUser()) { // <- state の変更を含む hooks がレンダリング前に実行される
    router.push('/');
    return null;
  }

  return <>{children}</>;
}

コンポーネント直下ではなく、 useEffect 内で呼び出すようにすればOK

/Component/Auth.tsx

export const Auth: VFC<AuthProps> ({ children }) => {
  const [loading, setLoading] = useState<boolean>(true);
  const router = useRouter();
  const { isUser } = useUser();
  
  useEffect(() => {
    if (!router.isReady) {
      return;
    }

    // state にも SessionStorage にもデータがない場合はリダイレクト
    if (!isUser()) {
      router.push('/');
      return;
    }
    
    // use 情報の確認が完了するまで loading のままにする
    setLoading(false);
  }, [router.isReady]);


  return loading? <Loading /> : <>{children}</>;
}

そもそも作った hooks があまり良くない気もしますが、これで Warning の発生しないリロードしても user 情報が保持されて、user 情報がなければ / にリダイレクトするコンポーネントが作れました!
₍ ᐢ. ̫ .ᐢ ₎ ワーイ

所管

Next.js とか recoil とか色々使ってたけど、単純に React Hooks の使い方の問題でした。

React とか Next.js とか個人制作の雰囲気で触ってるから良い方法なのかダメな方法なのか判断ができないので、ちゃんとしたフロントエンジニアのチームでコードレビュー受けたい〜


[参考]

ハマった時は、これもアイカツ!を思うと頑張れる!!