DEV Community

Origami
Origami

Posted on

HoCとStorybook/addon-infoの落とし穴

TL;DR

Higher-Order Componentsをaddon-infoで表示させようとするとバグる

Storybook? Storybook/addon-info?

みなさん、storybookを使ってますか?コンポーネントを作っていく時に便利すぎるので是非使ってください。説明だるいのでプロジェクトページのexamples見ればすぐわかりの取得ができます。

さて、storybookのプラグインであるaddon-infoも超便利です。コンポーネントがどのような役割を果たすのか、何を意図して作られたのかをmarkdownで記述すると表示してくれますし、実際にstoryのコード内でどのように使われているかを表示したり、Flowによる型チェックが(たぶんTypeScriptも)定義されていた場合、それも表示してくれます。

たとえば、以下のようなコンポーネントがあるとします。

// @flow
import React from 'react';

type Props = {
    /* クエスチョンマークの前につく文字列です */
    label: string,
    /* クエスチョンマークの数 */
    amount: number,
};

/* めっちゃクエスチョンマークをあれします */
const SuperQuestionLabel = ({label, amount}: Props) => (
    <a>{label + ''.repeat(amount)}</a>
);

SuperQuestionLabel.defaultProps = {
    label: "",
};

export default SuperQuestionLabel;

Enter fullscreen mode Exit fullscreen mode

そして、story用のコードは次のようになります。

// @flow
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';

import SuperQuestionLabel from "../SuperQuestionLabel";

storiesOf('何もわからん', module)
    .add(
        'basic usage',
        withInfo(
            '感動する文章'
        )(() => (
            <SuperQuestionLabel
                label={"これからは飲酒の時代"}
                amount={8} />
        )))


Enter fullscreen mode Exit fullscreen mode

すると、storybookの当該ページにはInfoボタンが表示され、クリックすると次のようなすばらしいインフォメーション情報が表示されます。

感動する文章、コンポーネントがどのように使われるのかの一例、そしてプロパティの詳細情報、感動ですね。Reactに限らず、今やReactコンポーネントを作るのであればstorybook、そしてaddon-infoは欠かせないものになりつつあります。(残念な点としては、今の所addon-infoはReactにしか対応してない点でしょうか)

Main Issue

そんなaddon-infoには天敵が存在します。Higher-Order Componentsです。

その例として、Stateless Functional Componentsで、recomposeの機能を使用している場合です。

うまくいかない例を見てみましょう。

// @flow
import React from 'react';
import {pure} from 'recompose';

type Props = {
    /* クエスチョンマークの前につく文字列です */
    label: string,
    /* クエスチョンマークの数 */
    amount: number,
};

/* めっちゃクエスチョンマークをあれします */
const SuperQuestionLabel = ({label, amount}: Props) => (
    <a>{label + ''.repeat(amount)}</a>
);

SuperQuestionLabel.defaultProps = {
    label: "",
};

export default pure(SuperQuestionLabel);

Enter fullscreen mode Exit fullscreen mode

recomposeのpureを用いてコンポーネントの再レンダリングを抑えています。高いパフォーマンスを求められるWebアプリケーションであれば、pureonlyUpdateForKeysを用いてチューニングを行うことも多々あるでしょう。しかし、StorybookのInfoページは次のようになってしまいます。

感動どころか、失望してしまいます。

なぜこのようになるか?理由はこれらのpureonlyUpdateForKeysHigher-Order Componentsだからです。するとひとつコンポーネントにコンポーネントがラップされた形になってしまいますから、そのためにaddon-infoが各種情報を拾ってくれなくなるのです。

Solution

HoCしたコンポーネントであることが問題なのですから、素のコンポーネントをstoriesに記述すればいいわけです。

つまり、次のようなコードにします。

// @flow
import React from 'react';
import {pure} from 'recompose';

type Props = {
    /* クエスチョンマークの前につく文字列です */
    label: string,
    /* クエスチョンマークの数 */
    amount: number,
};

/* めっちゃクエスチョンマークをあれします */
const SuperQuestionLabel = ({label, amount}: Props) => (
    <a>{label + ''.repeat(amount)}</a>
);

SuperQuestionLabel.defaultProps = {
    label: "",
};

export const SuperQuestionLabel_ = SuperQuestionLabel;
export default pure(SuperQuestionLabel);
Enter fullscreen mode Exit fullscreen mode

export constを使用して、素のコンポーネントを追加で出力するようにしただけです。

そして、story用のコードも、importするものを変更すればいいのです。

// @flow
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';

// ここを変更!
import {SuperQuestionLabel_} from "../SuperQuestionLabel";

storiesOf('すべてが理解できた', module)
    .add(
        'basic usage',
        withInfo(
            '感動する文章'
        )(() => (
            <SuperQuestionLabel_
                label={"これからは飲酒の時代"}
                amount={8} />
        )))

Enter fullscreen mode Exit fullscreen mode

少々(exportする名前が)雑ではありますが、これでaddon-infoは次のように、期待通りの表示をしてくれます。

感動!君も泣け

Conclusion

Higher-Order Componentsをdefaultで出力しているコンポーネントは、少なくともStorybookのaddon-infoで概要を表示させたいのであれば、素のコンポーネントもexportしてあげよう

END OF FILE

最後の画像のAlt textは「感動!君も泣け」です。

そして私はこの問題におもいっきりハマり、2日無駄にしました。人生は短く、みなさんの人生も短い、だから私は人生の時間をさらに削り、みなさんの人生の時間を無駄にしないよう、記事を書いている…

Oldest comments (0)