ゆめみのフロントエンドコーディング試験の題材で React の勉強をしました

ちょっと前にツイッターで見かけた、ゆめみのフロントエンドコーディング試験

「RESAS API を使用して、都道府県別の総人口推移グラフを表示するSPAを作る」っていうお題

React の勉強をするのにちょうどいい題材だなぁって思ったのでやってみた。課題を公開してるってことは「やってみてもいいよ」ってことかなと思ってるんだけど、もし違ったら GitHubリポジトリーを private にするので連絡ください

1週間でやらないといけないところを2ヶ月近くやってるし、コミットログも特に何も考えずにポイポイ書いたから、全然だめなんだけど、でも、色々勉強になったので、とてもよかった。楽しかったー!

つくったもの

こんな感じ

GitHub Pages にデプロイしておいた(動かすには RESAS API のキーが必要です)

コードはここ

以下メモ

プロジェクトのセットアップ

Vite + React 18 + TypeScript にした。パッケージマネージャーは npm で

  • Vite 使ってみたいなと思ってたので
  • Vite 起動が一瞬ですごく良い
  • 変更がすぐ反映されるので起動しっぱなしで開発してた

ESLint でチェックして Prettier でフォーマット

  • 仕事ではテンプレートが用意されてるので気にしたことがなくて、どんなルールがあるのかとか全然詳しくないので色んな記事を見ながら書いた。難しい・・・
  • airbnb のを基本にしてあとは適当に、default export を使わないようにしたり、arrow function にしたり、import 周りをごにょごにょしたりした

lint-stagedHusky を使って、Git の pre-commit hook を設定

  • Git のコミットをするときに自動で ESLint と Prettier が実行されるようにしといた。便利

ここまでで既に1週間以上経ってた気がするw

RESAS の API キーの扱い

課題は「RESAS API でデータを取得」なんだけど、RESAS API 呼び出しには API キーが必要

そのままアプリに API キーを埋め込んで公開してしまうと僕のキーが見えてしまうのでどうしようかなぁと考えて、RESAS API キーの入力画面を作ることにした

一応書いておくと、入力したキーは RESAS 以外にはどこにも送信されません

React Router

API キーの入力画面を作ったので、ルーティング用に React Router を導入してみた

  • 最初は <BrowserRouter> を使って /apikey ってパスに遷移してたんだけど、リロードしたときに 404 になっちゃうし、別にこのパス使わなくていいので最終的には <MemoryRouter> を使うことにした
export const App = () => (
  <MemoryRouter basename={import.meta.env.BASE_URL} initialEntries={[import.meta.env.BASE_URL]}>
    <AppThemeProvider>
      <ApiClientProvider>
        <AppRoutes />
      </ApiClientProvider>
    </AppThemeProvider>
  </MemoryRouter>
);

React Query

API の呼び出しには、気になってた React Query を使ってみた

  • キャッシュが強烈で便利。今回のアプリは、いちど呼び出した API のレスポンスを永遠にキャッシュするようにしたので、グラフのチェックボックスを外したりチェックしたりしても2回目以降は API は呼び出さずにキャッシュから取得される
  • API キーをクリアした場合には、キャッシュをリセットするようにしてる。画面をリロードしてもキャッシュはクリアされる(状態はどこにも保存していない)
  • useQuery はフックなので if の中で使ったりはできなくて、分岐したい場合は別のコンポーネントにする必要があるのね
  • useQueries で選択されている全都道府県のデータを取得するようにした↓。面白かった
export const usePopulationsQueries = (prefectures: Prefecture[]) => {
  const resasClient = useResasClient();
  return useQueries(
    prefectures.map((it) => ({
      queryKey: ['population', it.prefCode],
      queryFn: () => resasClient.fetchPopulations(it.prefCode),
      select: (populations: PopulationPerYear[]): PrefecturePopulation => ({
        ...it,
        populations,
      }),
    })),
  );
};
  • ローディング中の表示に最初は Suspense 使ってたんだけど、ちょっと今回のアプリの作りだと違和感があったので、素直に isLoading を使うことにした
  • 一方で、エラーハンドリングは ErrorBoundary を使うことにした
  • なので QueryClient の初期化はこんな感じになった↓
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      refetchOnWindowFocus: false,
      cacheTime: Infinity,
      staleTime: Infinity,
      useErrorBoundary: true,
    },
  },
});

Emotion

CSS には EmotionStyled Components を使うことにした

  • せっかくなので Global StyleTheme も使ってみた。便利ね
export const Headline = styled.h2(
  ({ theme }) => css`
    ${theme.fonts.headlineM}
  `,
);
  • フォントのサイズ指定は rem でやってみた
  headlineM: {
    fontSize: `${28 / 16}rem`,
    lineHeight: `${36 / 16}rem`,
    fontWeight: '500',
  },

共通コンポーネント

CSS フレームワーク使っちゃだめって書いてあったのでどうしようかなぁって思って Material Design 3 を参考にして、適当に共通コンポーネントみたいなものを作ってみた

HighCharts

グラフの描画には HighCharts を使った

  • そのままより、ちょこっと手を入れた方が勉強になるかなぁと思って、色を変えたりラベルを変えたりして遊んだ
    • デフォルトだと、チェック入れ直すたびに違う色になってしまうから、そうならないようにしておいた
  • データ生成用のフックを作っておいた
export const useHighCharts = (theme: Theme) => {
  const options = useMemo(() => createOptions(theme), [theme]);
  return { options, colors, markerSymbols, minYear, maxYear };
};

レスポンシブデザイン

課題に「スマホ表示に対応すること」って書いてある

  • HighChart はレスポンシブ対応してるので、対応しないといけないのは都道府県のチェックボックスの部分だけ
  • グリッドレイアウトを使うことにした。特に何も考えなくてもレスポンシブ対応になった。わいわい
const PrefectureList = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
  grid-gap: 1px;
`;

Mock Service Worker

モックサーバーとして MSW を入れた

  • ローカルで開発してるときに毎回 API キー入れたり、RESAS API を呼び出したりするの嫌だなと思って
  • dev ってキーで OK にして、近畿地方の情報だけ返すように作っておいた
export const mockPrefectures: ResponseResolver<MockedRequest, typeof restContext> = (req, res, ctx) => {
  if (req.headers.get('x-api-key') !== 'dev') {
    return res(ctx.status(200), ctx.json({ statusCode: '403', message: 'Forbidden.', description: '' }));
  }
  return res(
    ctx.status(200),
    ctx.json({
      message: null,
      result: [
        { prefCode: 24, prefName: '三重県' },
        { prefCode: 25, prefName: '滋賀県' },
        { prefCode: 26, prefName: '京都府' },
        { prefCode: 27, prefName: '大阪府' },
        { prefCode: 28, prefName: '兵庫県' },
        { prefCode: 29, prefName: '奈良県' },
        { prefCode: 30, prefName: '和歌山県' },
      ],
    }),
  );
};

Storybook

UI コンポーネントのチェックをするのに Storybook を導入しておいた

  • 開発中に Vite でアプリを起動してるのと同時に、Storybook も起動して確認しながらコードを書いてた。便利

  • いったん書き終わったあとに CSF 3.0 ( Component Story Format ) ってフォーマットが導入されようとしてる?ことに気づいたので、せっかくなので CSF 2.0 から書き直した
export const Default: ComponentStoryObj<typeof Target> = {
  decorators: [
    (Story) => (
      <ApiClientProvider initialResasApiKey="dev">
        <PageLayout onClickBackButton={() => {}}>
          <Story />
        </PageLayout>
      </ApiClientProvider>
    ),
  ],
};

export const OnLoadingPrefectures: ComponentStoryObj<typeof Target> = {
  ...Default,
  args: {
    isLoadingPrefecturesParam: true,
  },
};
  • CSF 3.0 の方が僕は分かりやすくて好きだなぁって思った

ちなみに、Storybook は React 18 対応がまだなのかな?なので npm 実行するときに --legacy-peer-deps をつけたりしてた。22 high severity vulnerabilities って言われたりするから、仕事で使うならこの辺詳しく見ておかなきゃなんだろうけど、今回はいいかなと思ってスルーした!

Chromatic

Storybook はローカルだけでも便利なんだけど Chromatic を使うと

  • 差分をチェックできたり
  • Storybook をオンラインでチェックできたりして

とても便利なので、やっておいた。main ブランチの Storybook は↓にデプロイされてる

なんかちょっと不安定。キャッシュがきついのかな?って感じの動きをする

CircleCI

Chromatic への Storybook のデプロイと GitHub Pages へのアプリのデプロイは、CircleCI でやっておいた。CircleCI 便利なのでぜひ使ってくださいー!(宣伝

GitHub Pages へのデプロイは、vite build で生成したファイルを docs ディレクトリに入れてコミット&プッシュをしてる。そうすると、あとは GitHub がページを公開してくれる。このときに [skip ci] をコミットメッセージにいれてないと、そのプッシュでまた CircleCI がトリガーされてずっと動き続けるから気をつけてね

      - run:
          command: |
            git add docs
            if ! git diff --staged --quiet --; then
              git commit -m "[skip ci] Update GH Pages"
              git push origin main
            fi

フォルダ構成

コンポーネントをどこに置くかとか、フォルダを作るかとか、index.tsx にするか ComponentAbc.tsx にするかとか色々悩んで、結局適当にした!(おい

テスト

と、ここまでで、なんか満足してしまったので、終わりにした

やったらいいかなと思ってたのは、この辺:

  • Jest によるフックの UT
  • Jest と Storybook を連携したコンポーネントの UT
  • Cypress による画面の動作テスト

とてもお世話になった方々

piyoko さん。Vite + React + TypeScript 周りでいっぱい参考にさせてもらいました。ありがとうございます!

zenn.dev

よしこさん。コンポーネントディレクトリ構成や、ESLint などの設定を何度も読ませてもらいました。ありがとうございます!

zenn.dev

Seya さん。React のアンチパターンや Husky などの記事が勉強になりました。ありがとうございます!

zenn.dev

Takepepe さん。JS 周りの最新情報など、だいたい Takepepe さんのところにたどり着いてました。ありがとうございます!

zenn.dev

そして、ゆめみさん。とてもおもしろい題材をありがとうございました!

楽しかった!