React + TypeScript + Vite で babel-plugin-react-intl-auto を使ってみる

2022/05/30に公開

はじめに

React で開発をするときには公式で説明されている Create React App を使うのが一般的だと思いますが、Create React App は簡単なゆえにカスタマイズが難しいという点があります。たとえば

  • Create React App が Jest に依存しているのでバージョンを変更できない
  • Create React App の内部の設定を書き換えられない (eject するか react-app-rewired や craco などのツールを使う必要がある)
  • 単純にビルドが遅い

というようなことがあります。この問題を解決するために Create React App ではなく Vite に移行する流れが起きています。初級者が使うには Create React App は便利なので必ずしも劣っているということではないと思いますが、中級者以上になってある程度いろいろカスタマイズしたいということになったら Vite に移行するのも良いと思います。

React で i18n をするときに (あるいは単に文字列をリソース管理したいときに) react-intl を使っていますが、react-intl を使いやすくするための babel-plugin-react-intl-auto というプラグインがあります。詳しくは作者の記事をご覧ください。

https://qiita.com/akameco/items/ccf32dedb3630f774358

これを Vite で使えるようにします。

サンプル コード

https://github.com/karamem0/samples/tree/main/react-vite-with-babel-plugin-react-intl-auto

実行環境構築

まずは Vite でプロジェクトを作成します。後述しますが Storybook が現時点では React 18 に対応していないため React 17 の最新版である vite@2.9.0 を使用します。

npm create vite@2.9.0 my-application -- --template react-ts

react-intl を追加します。

npm install react-intl

babel-plugin-react-intl-auto を追加します。合わせて extract-react-intl-messages も入れると幸せになれます。

npm install babel-plugin-react-intl-auto extract-react-intl-messages --save-dev

TypeScript が babel-plugin-react-intl-auto を認識できるように tsconfig.json を修正します。

    "include": [
      "src",
+     "node_modules/babel-plugin-react-intl-auto/**/*.d.ts"
    ],

Vite が babel-plugin-react-intl-auto を認識できるように vite.config.json を修正します。

  export default defineConfig({
    plugins: [
      react(
+     {
+       babel: {
+         plugins: [
+           'react-intl-auto'
+         ]
+       }
+     }
    )]
  })

extract-react-intl-messages を実行できるように babel.config.js を作成します。

module.exports = {
  plugins: [
    'react-intl-auto'
  ]
};

src/messages.ts を作成します。

import { defineMessages } from 'react-intl';

const messages = defineMessages({
  HelloWorld: 'こんにちは世界'
});

export default messages;

src/translations/ja.json を生成します。

npx extract-messages -l ja -o src/translations --flat src/messages.ts

src/translations/ja.json を修正します。

{
  "src.HelloWorld": "こんにちは世界"
}

src/translations.ts を作成します。

import ja from './translations/ja.json';

const translations: { [key: string]: Record<string, string> } = {
  ja
};

export default translations;

src/App.tsx を修正します。

- import { useState } from 'react'
+ import React, { useState } from 'react'
  import logo from './logo.svg'
  import './App.css'
+ import { FormattedMessage, IntlProvider } from 'react-intl'
+ import translations from './translations'
+ import messages from './messages'
  
  function App() {
    const [count, setCount] = useState(0)
  
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>Hello Vite + React!</p>
          <p>
            <button type="button" onClick={() => setCount((count) => count + 1)}>
              count is: {count}
            </button>
          </p>
          <p>
            Edit <code>App.tsx</code> and save to test HMR updates.
          </p>
          <p>
            <a
              className="App-link"
              href="https://reactjs.org"
              target="_blank"
              rel="noopener noreferrer"
            >
              Learn React
            </a>
            {' | '}
            <a
              className="App-link"
              href="https://vitejs.dev/guide/features.html"
              target="_blank"
              rel="noopener noreferrer"
            >
              Vite Docs
            </a>
          </p>
+         <p>
+           <IntlProvider locale='ja' messages={translations.ja}>
+             <FormattedMessage {...messages.HelloWorld} />
+           </IntlProvider>
+         </p>
        </header>
      </div>
    )
  }
  
  export default App

npm run dev を実行し http://localhost:3000 にブラウザーでアクセスします。「こんにちは世界」と表示されることを確認します。

テスト環境構築

テストは Jest と Storybook で行います。

Jest

先述した通り、Create React App と異なり Vite には Jest が含まれないので、インストールするところからはじめます。TypeScript で Jest をする場合は ts-jest を使用することが多いですが、ts-jest では Babel プラグインの読み込みがうまくいかなかったため、babel-jest を使用します。また、Babel の関連するプリセットも追加します。

npm install jest jest-environment-jsdom @types/jest @testing-library/react@12 @testing-library/jest-dom --save-dev
npm install babel-jest @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript --save-dev

jest.config.js を作成します。

module.exports = {
  testEnvironment: 'jsdom',
  testMatch: [
    '**/*.test.ts',
    '**/*.test.tsx'
  ],
  moduleNameMapper: {
    "\\.(css|svg)$": '../jest.mock.js'
  }
};

jest.mock.js を作成します。CSS と SVG をモックするだけなので中身は空です。

module.exports = {};

babel.config.js を修正します。Jest の公式サイトにも説明がある通りターゲットとして node を指定する必要があります。

https://jestjs.io/ja/docs/getting-started

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          node: "current"
        }
      }
    ],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  plugins: [
    'react-intl-auto'
  ]
};

src/App.test.tsx を追加します。

import React from 'react';

import { render, screen } from '@testing-library/react';
import App from './App';

describe('App', () => {

  it('create shapshot', () => {
    render(<App />);
    expect(screen.queryAllByText(/^.*$/)[0]).toMatchSnapshot();
  });

});

npx jest を実行します。スナップショットが作成されることを確認します。

Storybook

Storybook には Vite 向けのビルダーがあり簡単に構築することができます。

npx sb init --builder @storybook/builder-vite

Babel プラグインを有効にするには .storybook/main.jsviteFinal を設定します。

https://github.com/storybookjs/builder-vite/issues/286

  viteFinal: (config) => {
    const react = require("@vitejs/plugin-react");
    config.plugins = [
      ...config.plugins.filter((plugin) => {
        return !(
          Array.isArray(plugin) && plugin[0].name === "vite:react-babel"
        );
      }),
      react({
        exclude: [/\.stories\.(t|j)sx?$/, /node_modules/],
        babel: {
          plugins: [
            'react-intl-auto'
          ]
        },
      }),
    ];
    return config;
  }

src/App.stories.tsx を作成します。

import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import App from './App';

export default {
  title: 'App',
  component: App,
} as ComponentMeta<typeof App>;

const Template: ComponentStory<typeof App> = (args) => <App />;

export const Primary = Template.bind({});
Primary.args = {};

npm run storybook を実行すると「こんにちは世界」と表示されます。

おわりに

Storybook については @storybook/builder-vite のメンテナーである Ian さんに教えていただきました。ありがとうございました!

https://twitter.com/IanVanSchooten/status/1529542219530911744

Discussion