🐣

StorybookでTextFieldのStoryを作るときに悩んだこと

2023/06/04に公開1

主旨

StorybookでTextFieldのようなvalueを持つコンポーネントのStoryを作るときに少し悩んだので、その解決策を紹介します。

この記事の対象読者

React、Storybookの初学者。

今回使用したライブラリーのバージョン

  • React: 18.2.0
  • TypeScript: 4.9.5
  • MUI: 5.13.3
  • Storybook: 7.0.18

環境構築

以前書いたこちらの記事参照。

valueを持たないコンポーネントのStory

Storybookをインストールしたときに提供されるExample/Buttonなどはこれに該当します。以下のようにStoryを作成すると、ブラウザで見た目を確認できます。

export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
};

valueを持つコンポーネントのStory(うまくいかない例)

次のようなMUIのTextFiledのラッパーを作成したとします。

import { TextField } from "@mui/material";

type MyTextFieldProps = {
    variant: 'outlined' | 'filled' | 'standard';
    label: string;
    value: string;
    onChange?: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
}

export function MyTextField(props: MyTextFieldProps) {
    const {variant, label, value, onChange} = props;
    return (
        <TextField variant={variant} label={label} value={value} onChange={onChange} />
    );
}

MyTextFieldを呼び出す側は以下のような処理になります。

// 省略
const [value, setValue] = useState('');

return (
    <MyTextField variant={'outlined'} label={'ラベル'} value={value} onChange={(e) => setValue(e.target.value)}>
);

このときMyTextFieldのStoryをExample/Buttonと似たように書くとうまくいきません。例えば以下のStoryはよくない例です。ブラウザで入力内容を変更しようとしても変更できません。

// MyTextFieldProps.valueは必須の項目なため何かしら渡さないと怒られる。そのため変数を定義して渡してみた。
let value = 'value';

export const Outlined: Story = {
  args: {
    variant: 'outlined',
    label: 'ラベル',
    value: value,
    onChange: (e) => {value = e.target.value},
  },
};

「じゃあ、MyTextFieldを呼び出す側の処理と同じようにuseState()を利用しよう!」と考え、以下のようにコードを変更すると、React Hook "React.useState" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. のエラーが出てしまいます。

// useState()を利用するよう変更
// let value = 'value';
const [value, setValue] = useState('');

valueを持つコンポーネントのStory(うまくいく例)

上記エラーに対応するため以下のようにコードを変更します。

  • renderプロパティに関数コンポーネントを設定する(この関数コンポーネント内ではuseState()を使える)。なお、アロー関数を利用した場合、React Hook "useState" is called in function "render" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". のエラーが発生したため関数式を利用しています。以前の環境ではこのエラーは発生しなかったのですが・・・
  • ファイルの拡張子を.tsから.tsxに変更(拡張子変更後はStorybookの再起動が必要かもしれません)
  • <MyTextField> を利用せずに <meta.component> を利用しているのはちょっとした工夫。こうすればこのファイルを元に別ファイルを作るときにも修正箇所が少なくなる
export const Outlined: Story = {
  args: {
    variant: 'outlined',
    label: 'Text Field',
    value: ''  // valueは必須の項目なため何かしらの値を渡す(この値は実際には利用しない)
  },
  render: function Comp({ ...args }){  // 関数コンポーネントを定義
    const [value, setValue] = useState('');  // 関数コンポーネント内なのでuseState()が使える

    return (
      <meta.component
        {...args}
        value={value}
        onChange={(e) => setValue(e.target.value)}
      ></meta.component>
    );
  },
};

まとめ

  • valueを持つコンポーネントのStoryではrenderプロパティに関数コンポーネントを設定するとよい。その際、拡張子を.tsxに変更する必要がある(必要に応じてStorybookを再起動)
  • 上記関数コンポーネントが返却するコンポーネントは <meta.component> と記述すると変更が少し楽そう
  • 他にいい方法をご存じの方がいれば教えて下さい。コンポーネントに対してuseRef()を使う方法もありそうだなとは思います

Discussion