hygenでテンプレートからReactコンポーネントを生成する
hygenでReactコンポーネントを生成する
昨年、React コンポーネントの作成を自動化するためにシェルスクリプトを書きました(「Reactコンポーネントの雛形生成のシェルスクリプトを書いた」)。
その後、「hygen で生成 - 対話形式の Component 雛形 -」 という記事でファイル生成ツール hygen を知り、とても便利だったため本業のプロジェクトに導入しました。
本記事ではコンポーネント生成のために利用しているテンプレートの紹介と使い方、そして導入した利点を紹介します。
hygenのReactコンポーネント用のテンプレートの紹介
hygen とは、マークダウンのような frontmatter とテンプレートエンジン ejs からなるテンプレートを元に新しいファイルを生成するツールです。
以下のコマンドで hygen をインストールし、サンプル用のテンプレートを生成しましょう。
$ npm i -g hygen
$ hygen init self
コマンド実行後、プロジェクトルートに_templates
ディレクトリが作成されます。
_templates
└── generator
├── help
│ └── index.ejs.t
├── new
│ └── hello.ejs.t
└── with-prompt
├── hello.ejs.t
└── prompt.ejs.t
本業のプロジェクトではこれを改変して、以下のような構成にしています。
_templates
└── component
└── new
├── component.ejs.t
├── index.ejs.t
├── index.stories.ejs.t
├── prompt.js
└── style.module.ejs.t
これで React コンポーネント、Storybook、scss、index.ts を作成できます。
hygenのテンプレートファイル
各ファイルの中身を紹介します。
React コンポーネント
---
to: src/components/<%= dir %>/<%= h.changeCase.pascal(name) %>/<%= h.changeCase.pascal(name) %>.tsx
---
import React from 'react'
import css from './style.module.scss'
type ContainerProps = unknown
export type Props = unknown
export const Component: React.FC<Props> = (props) => (
<></>
)
const Container: React.FC<ContainerProps> = (props) => {
return <Component {...props} />
}
Container.displayName = '<%= h.changeCase.pascal(name) %>'
export default Container
冒頭はファイルパスとファイル名の指定です。
---
to: src/components/<%= dir %>/<%= h.changeCase.pascal(name) %>/<%= h.changeCase.pascal(name) %>.tsx
---
dir
と h.changeCase.lower(name)
でファイルパスを指定し、h.changeCase.pascal(name)
でファイル名を決定します。
h.changeCase.lower
やh.changeCase.pascal
は hygen が用意している関数です。他にも多くの種類があるため、 柔軟に対応できます。
変数に格納する値を指定する方法は後述のprompt.js
の箇所で紹介します。
なお、React コンポーネントは「経年劣化に耐える ReactComponent の書き方」 を参考にしています。
またComponent
(Presentation Component)とProps
を named export しているのは、下記の Storybook で利用するためです。
Storybook
---
to: src/components/<%= dir %>/<%= h.changeCase.pascal(name) %>/index.stories.tsx
---
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { Component as <%= h.changeCase.pascal(name) %>, Props } from './<%= h.changeCase.pascal(name) %>'
export default {
title: '<%= h.changeCase.pascal(name) %>',
} as Meta<Props>
const Template: Story<Props> = ({ ...args }) => <<%= h.changeCase.pascal(name) %> {...args} />
export const Default = Template.bind({})
Default.args = {
}
Storybookのファイルの雛形はStorybook 公式サイト の React + TypeScript の書き方を参考にしています。
import React from 'react';
import { Story, Meta } from '@storybook/react';
import { ButtonGroup, ButtonGroupProps } from '../ButtonGroup';
//👇 Imports the Button stories
import * as ButtonStories from './Button.stories';
export default {
title: 'ButtonGroup',
component: ButtonGroup,
} as Meta;
const Template: Story<ButtonGroupProps> = (args) => <ButtonGroup {...args} />;
export const Pair = Template.bind({});
Pair.args = {
buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }],
orientation: 'horizontal',
};
前出のテンプレートファイルと見比べるとほとんど同じことがわかります。
ただ、Props
の型はコンポーネントのファイルから export せず、ComponentProps
で取得する方がスマートかもしれません。
import { Meta, Story } from '@storybook/react'
import React, { ComponentProps } from "react"
import { Component as <%= h.changeCase.pascal(name) %>, Props } from './<%= h.changeCase.pascal(name) %>'
type Props = ComponentProps<typeof <%= h.changeCase.pascal(name) %>>
// ...
scss
---
to: src/components/<%= dir %>/<%= h.changeCase.pascal(name) %>/style.module.scss
---
@import 'src/assets/scss/lib/color';
@import 'src/assets/scss/lib/variable';
@import 'src/assets/scss/lib/icon';
@import 'src/assets/scss/lib/function';
@import 'src/assets/scss/lib/mixin';
各ファイルで共通の scss ファイルを import しています。
export
---
to: src/components/<%= dir %>/<%= h.changeCase.pascal(name) %>/index.ts
---
export { default } from './<%= h.changeCase.pascal(name) %>'
default export をしていますが、もちろん書き方を変えると named export に変更できます。
prompt.jsで入力値を決める
module.exports = [
{
type: 'select',
name: 'dir',
message: 'ディレクトリを選択してください',
choices: ['common', 'features', 'pages'],
},
{
type: 'input',
name: 'name',
message: 'コンポーネント名を入力してください',
validate: (input) => input !== '',
},
]
対話型 CLI での値の受け取り方をprompt.js
の中で設定します。プロパティname
の値が ejs 内の変数名に対応します。
例えば、type
を select
に指定すると、列挙した値から選択することになります。
また、type
がinput
の場合は自由入力です。空欄は避けたいため、validate
にバリデーションを記述します。
hygenのコマンド実行によるファイル生成
src/components/common
ディレクトリにLayout
コンポーネントを作成してみましょう。
コマンド$ hygen component new
を実行すると選択肢が列挙されます。
ディレクトリ名をchoices
に設定した値から選択します。
$ hygen component new
? ディレクトリを選択してください …
❯ common
features
pages
次に、自由入力でコンポーネント名をLayout
と入力します。
$ hygen component new
✔ ディレクトリを選択してください · common
✔ コンポーネント名を入力してください · Layout
これでファイルが生成されました。
$ hygen component new
✔ ディレクトリを選択してください · common
✔ コンポーネント名を入力してください · Layout
Loaded templates: _templates
added: src/components/common/Layout/Layout.tsx
added: src/components/common/Layout/index.ts
added: src/components/common/Layout/index.stories.tsx
added: src/components/common/Layout/style.module.scss
記事の最後で生成したファイルの中身を掲載しています。
hygen導入のメリット
Storybook の書き方の統一と作成漏れの回避
Storybook のコンポーネントの書き方は柔軟なため、一通りではありません。しかし、テンプレートを用意すると大人数で開発していても記述方法の統一できます。
また、当然のことですが「Storybook First な開発」 をする際、Storybook コンポーネントは不可欠です。
テンプレートから一気に Storybook 用のファイルを生成すれば、作成漏れはありません。
デザイナーさんとの効果的な協業
全てのデザイナーさんが React に慣れているわけではありません。HTML と CSS をデザイナーさんが記述する場合、React で実装するという心理的なハードルをできるだけ下げたいです。
そこで、本記事で紹介したテンプレートを使うと責任範囲を限定できます。
つまり、デザイナーさんは Presentation Component に JSX を記述し、style.module.scss
に scss を書いて Storybook で動作確認をするだけで OK になるのです。
// ...
import css from './style.module.scss'
// ...
export const Component: React.FC<Props> = (props) => (
<></> // ここに JSX を記述する
)
デザイナーさんは React コンポーネントの Props の渡し方や、Storybook 用のコンポーネントの書き方を覚えずに済みます。実質的に、HTML と CSS(SCSS)の書き方を知っているだけで作業に取り掛かれるからです。
まとめ
IntelliJ IDEA にテンプレートを用いたファイル作成機能はありますが、ファイルは1つずつしか生成できません。
hygen は複数ファイルを同時に生成したい場面で威力を発揮します。例えば JS を利用しないプロジェクトであっても hygen を導入してもいいかもしれません。Class と Interface、Test 用のファイルを同時に生成するといったケースであっても有用だからです。
また、『UNIXという考え方』(Amazon) という書籍の中で「定理8: 過度の拘束的インターフェースは避ける」と書かれています。
一旦、そのアプリケーションをコマンドインタープリタから起動すると、そのアプリケーションが終了するまでコマンドインタープリタとの対話ができなくなる。ユーザーはアプリケーションのユーザーインターフェースの内部に拘束され、拘束を解くための行動を起こさない限り、その拘束から逃れられない。
『UNIXという考え方』(Mike Gancarz 著、芳尾桂 監訳。オーム社)
hygen はとても柔軟なので様々な指定が可能ではあるものの、対話数はできるだけ絞っておくと良いでしょう。
生成したファイル一覧
React コンポーネント
import React from 'react'
import css from './style.module.scss'
type ContainerProps = unknown
export type Props = unknown
export const Component: React.FC<Props> = (props) => (
<></>
)
const Container: React.FC<ContainerProps> = (props) => {
return <Component {...props} />
}
Container.displayName = 'Layout'
export default Container
Storybook
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { Component as Layout, Props } from './Layout'
export default {
title: 'Layout',
} as Meta<Props>
const Template: Story<Props> = ({ ...args }) => <Layout {...args} />
export const Default = Template.bind({})
Default.args = {
}
scss
@import 'src/assets/scss/lib/color';
@import 'src/assets/scss/lib/variable';
@import 'src/assets/scss/lib/icon';
@import 'src/assets/scss/lib/function';
@import 'src/assets/scss/lib/mixin';
index.ts
export { default } from './Layout'
Happy Coding 🎉