ちょっと前にツイッターで見かけた、ゆめみのフロントエンドコーディング試験
「RESAS API を使用して、都道府県別の総人口推移グラフを表示するSPAを作る」っていうお題
React の勉強をするのにちょうどいい題材だなぁって思ったのでやってみた。課題を公開してるってことは「やってみてもいいよ」ってことかなと思ってるんだけど、もし違ったら GitHub のリポジトリーを private にするので連絡ください
1週間でやらないといけないところを2ヶ月近くやってるし、コミットログも特に何も考えずにポイポイ書いたから、全然だめなんだけど、でも、色々勉強になったので、とてもよかった。楽しかったー!
つくったもの
こんな感じ
これでおわりにするー pic.twitter.com/K8zhrRUp54
— Mitsuyuki Shiiba (@bufferings) June 11, 2022
GitHub Pages にデプロイしておいた(動かすには RESAS API のキーが必要です)
コードはここ
以下メモ
プロジェクトのセットアップ
Vite + React 18 + TypeScript にした。パッケージマネージャーは npm で
- Vite 使ってみたいなと思ってたので
- Vite 起動が一瞬ですごく良い
- 変更がすぐ反映されるので起動しっぱなしで開発してた
ESLint でチェックして Prettier でフォーマット
- 仕事ではテンプレートが用意されてるので気にしたことがなくて、どんなルールがあるのかとか全然詳しくないので色んな記事を見ながら書いた。難しい・・・
- airbnb のを基本にしてあとは適当に、default export を使わないようにしたり、arrow function にしたり、import 周りをごにょごにょしたりした
lint-staged と Husky を使って、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 には Emotion の Styled Components を使うことにした
- せっかくなので
Global Style
やTheme
も使ってみた。便利ね
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 を入れた
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 は↓にデプロイされてる
- https://main--6294e24b7a7308003a402696.chromatic.com/?path=/story/app-pages-apikeyinputpage--default
なんかちょっと不安定。キャッシュがきついのかな?って感じの動きをする
await 使わないようにしといた。ついでにフォントのサイズもちょっと小さくしたんだけど、差分が見れてChromatic便利ー。 pic.twitter.com/cQpyLPWdc4
— Mitsuyuki Shiiba (@bufferings) June 5, 2022
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 周りでいっぱい参考にさせてもらいました。ありがとうございます!
よしこさん。コンポーネントのディレクトリ構成や、ESLint などの設定を何度も読ませてもらいました。ありがとうございます!
Seya さん。React のアンチパターンや Husky などの記事が勉強になりました。ありがとうございます!
Takepepe さん。JS 周りの最新情報など、だいたい Takepepe さんのところにたどり着いてました。ありがとうございます!
そして、ゆめみさん。とてもおもしろい題材をありがとうございました!
楽しかった!