20211202のReactに関する記事は24件です。

esbuild + React(TS) で実現する超軽量な開発環境

自ブログからの転載になります。 今回アドベントカレンダーに初めて参加いたします! 概要 最近私が 0=>1 から開発に取り組んでいるプロダクトのフロントエンドで esbuild + React(TS) を利用した開発環境を選定しており、タイトルが若干盛っていることはさておき、リリースから少し時間もたったので、一度振り返って知見が共有できればと思います。 尚、今回ご紹介する開発環境はサンプルレポジトリ esbuild-dev-environment-sample を作成しましたので、適宜ご参照ください。 esbuildとは? この記事を見ている方はすでにご存知の通り、Webpackの100倍早いバンドラと謳われています。 その威力に関しては以前 記事 に書きましたので、ご参考いただけると幸いです。 もしまだ esbuild を試したことがない方がいましたら、是非 サンプルレポジトリ をcloneして npm i の後、npm build を試して見てください! かなり高速にビルドが完了することを実感できると思います。 選定理由 まずはesbuildを利用した開発環境を選定した理由を整理したいと思います。 私は今まで Angular, Nuxt, Next など各種SPAを利用してフレームワークを利用してプロダクトの 0 => 1 構築を行ってきました。その経験上、フレームワークは多くのツールチェーンを事前作業なしに利用でき、ビルド設定も高度に最適化されているため大変開発者が救われる物ではありますが、反面オーバーヘッドが大きく徐々にビルド時間が増大することで開発のアジリティ低下に悩まされやすいのも事実として実感してきました。 現実問題、ほとんどの開発者は複数のタスクを持っていることが多く、PCの性能の限界を考えてもタスクの切り替えは頻繁に発生し、その度にビルドに時間を取られていてはDXが悪化していく危険性があります。大それた言い方ですが、ビルド速度はフロントエンド開発のDXにおける長年のクリティカルな問題点だと言えます。 そこで、最近では(React系では) Preact や Gatsby の様な軽量な環境が流行っている事実があります。SSR対応の必要等があれば依然としてフレームワークの恩恵は大きいですが、Jamstack の様なツール依存の少ない環境ではより軽量な開発環境も好まれます。ただやはり、ビルドに関してのボトルネックは Webpack である事が多くここを改善するのが最も効果的だと思われます。 そのため、ビルド自体を高速化でき、既に多くの人気を得ている esbuild を利用したいと考えました。 考慮点としては、 Next や Gatsby などには対応していないため、ツールに依存しない開発を行うか、 vite の様な対応する新興ツールを選定する必要があります。 今回私が構築したプロダクトは、業務アプリケーションで所謂 Jamstack なSPAアプリケーションでした。 0 => 1 段階で漸進的にツールを取り入れていく運用が可能かつ、ある程度React系のフロントエンドの開発に慣れがあることから思い切ってフレームワークを捨て、esbuild + React で開発環境を構築してみました。 ビルドの設定 esbuildのビルド設定は非常にシンプルです。Webpackの利用経験などがある方であれば下記のコードで理解できると思います。 build.ts import * as fs from 'fs'; import * as path from 'path'; import { build, BuildOptions } from 'esbuild'; // 環境変数を確認 const NODE_ENV = process.env.NODE_ENV ?? 'development'; const isDev = NODE_ENV === 'development'; const watch = process.env.WATCH === 'true' || false; const metafile = process.env.META_FILE === 'true' || false; // webpackのdefine pluginと同じ const define: BuildOptions['define'] = { // コード上の `process.env.NODE_ENV` を `development` などで置き換える 'process.env.NODE_ENV': JSON.stringify(NODE_ENV), }; // ビルド処理 build({ define, // Reactのメインファイル entryPoints: [path.resolve(__dirname, 'src/index.tsx')], bundle: true, // ビルドされたバンドルの出力先 outfile: 'public/index.js', minify: !!process.env.MIN || !isDev, sourcemap: true, platform: 'browser', target: ['chrome58'], treeShaking: true, watch: watch && { // watchモードで起動したい場合は、再ビルドのcallbackを渡す onRebuild(err, result) { console.log(`${dayjs().format('HH:mm:ss')}: 再ビルド`); }, }, }).then(result => { console.log(`ビルド完了`); }).catch(() => process.exit(1)); その他, 基本的な loader(data-url, fontなど) や, external を指定する事もできます。 ローカルでの開発 結論から、以下の3段階の設定を行いました。 esbuildをwatchモードで起動 生成物をexpressサーバでホスト コード変更時にBrowsersyncでブラウザを自動でreload 1. 生成物をexpressサーバでホスト 環境変数 WATCH=turu の時に、差分ビルドされる様にビルド設定を書きます(前節の通り)。esbuildは差分ビルドも超高速なので、大抵の場合はコードの変更を検知して一瞬でビルドが完了します。 2. 生成物をexpressサーバでホスト 今回はpureなReact SPAであり、htmlファイルはindex.html一つだけになる想定をしています。そのため、全てのアクセスは /index.html に飛ばす(rewriteする)必要があり、http-server などで単純にホストするだけでは要件を満たせません。 そこで、expressを利用してローカルでホスティングをしてあげる事にしました。 expressのコードは以下の通りです。 local-hosting.ts import express from 'express'; import * as path from 'path'; const app = express(); const PORT = 3030; app.use(express.static(path.join(__dirname, 'public'))); app.get('/*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.listen(PORT); console.log(`listen on port: ${PORT}`); この通り、簡単にホスティングが可能です。起動ポートが3030になっているのは 3 で説明する Browsersync との兼ね合いです。ちなみに esbuild によってホストされるファイルの内容が順次書き換えられていくので、express自体は起動しっぱなしでOKです。 3. コード変更時にBrowsersyncでブラウザを自動でreload 1, 2により、任意のパスでReactアプリにアクセス可能になりました。この状態でもOKなのですが、コードを変更した時にesbuildの再ビルドの完了を待ってから自分でブラウザをリロードする必要があります。esbuildにはいわゆるHMR(hot module replacement)の機能がなく、実装のされる見込みも薄い現状があります。 そこで、Browsersync を利用して自動的にブラウザをリロードする様にしました。コマンドは以下の通りです。 npm i browser-sync browser-sync start --notify=false --proxy 'http://localhost:3030' --files 'public/*' --port 3000 これにより http://localhost:3000 へのアクセスを browser-sync が受けて、3030番ポート で待っている express に接続しながら、public ディレクトリ配下のファイルの変更(=esbuildの再ビルド完了)を検知して、ブラウザを自動更新してくれます。 package.jsonにまとめる 一例ですが、この様に concurrently を利用してこの様にコマンドをまとめました。 これにより npm run dev コマンド一発で開発環境が立ち上がります。 package.json { "scripts": { "dev": "concurrently --prefix \"{time}\" -t \"HH:mm:ss\" -c \"bgCyan.bold,bgGreen.bold,bgGrey.bold\" -n \"esbuild,BrowserSync,Express\" \"npm run build:watch\" \"npm run browser:reload\" \"npm run serve:local\"", "build": "node --require esbuild-register build.ts", "build:watch": "WATCH=true npm run build", "browser:reload": "wait-on public/index.js && browser-sync start --notify=false --proxy 'http://localhost:3030' --files 'public/*' --port 3000", "serve:local": "node --require esbuild-register local-hosting.ts" } } TIPS 以下では開発に当たって工夫した点やハマった点を整理したいと思います。 URLに状態を反映する 今回の仕組みではコードの変更時に自動でブラウザのリロードが走りますが、毎回初期状態に飛ばされる仕様だと開発が手間です。そのため、検索条件やモーダルの開閉やなどの主要な global state はURLのクエリストリングなどと同期する仕様が望ましいです。 (その方が様々な状態への直リンクを提供できる点でプロダクトの使い勝手もよくなると思われます。) TSの即時実行に関して 折角なので、esbuildを実行するbuild.tsや、expressを実行するlocal-hosting.tsもTypeScriptファイルで書きたいところです。ただ、毎度ビルドするのは面倒なので、TypeScriptを直接実行するツールがあると便利です。古くから利用されている物であれば、ts-node がありますが、実行毎のトランスパイルで少し待たされます。 そこで、今回はトランスパイル作業をesbuildで行ってくれる esbuild-register を利用しました。これにより、個人的には気にならないくらいの速度でTSを直接実行する事ができました。 CSSの対応 esbuildはコード上でimportされているCSSファイルを全て一つのファイルにまとめて出力してくれますが、scss の変換や、 css modules など特殊なローダが必要になる様な機能を実装していません。いずれも事前にトランスパイルするとか、自作プラグインを作成するなどして対応も可能な様ですが、ひとまず普通のCSSで記述する様にしました。 ただし、各コンポーネントごとにのCSSはファイルを作成し、そのコンポーネントのルート要素のclass名を必ず頭につける運用である程度分離できる様にしました(コードをご参照ください)。私の場合はコンポーネントの雛形(index.tsx, index.css, index.spec.tsなど一式)を自動生成するCLIツールを作成してメンバーに共有しました。 (eslintの独自ルールを作っても良いかもしれません)。 大規模な開発を行う際は当然CSSのエンカプセレーションを検討すると思いますので、注意が必要なポイントです。 metafileによるバンドル解析 WebpackではBundle Analyzerを利用してバンドル結果を出力し、バンドルサイズのチューニングなどを行いますが、それと同様の事がmetafileによって実現されています。ビルド設定にフラグを立てるだけで、モジュール解決の詳細がまとめられた meta.json が出力されます。 Dead Code Eliminationに関して esbuildには tree shaking と dead code elimination 機能があります。これにより、実行されない事が静的に確定するコードブロックを削除してくれますが、以下の通り注意をする必要があります。 /* NODE_ENV が production の場合 */ if (process.env.NODE_ENV === 'development') { // to be eliminated import('module1').then( module => { /* some code */ } ); } else { // not to be eliminated import('module2').then( module => { /* some code */ } ); } const isDev = process.env.NODE_ENV === 'development'; if (isDev) { // not to be eliminated import('module3').then( module => { /* some code */ } ); } else { // not to be eliminated import('module4').then( module => { /* some code */ } ); } この通り実際のバンドルから除外されるmoduleは module1 だけになり、 module3 はバンドルされます。 例えば、私の場合 DIコンテナ(inversify.js)を利用しており、検証環境にだけ検索APIのモックがDIされますが、APIモックは個人情報っぽい顧客のダミーデータが記載されている状態でした。なんとなく気持ち悪いのと、容量的にも大きいのでこの点を注意しました。modulがbundleされているかどうか は上記の meta.json で確認できます。 Jestに関して 設定ファイルを以下の通りしました。TSのトランスパイル(jestのtransform)には ts-jest が使えますが動作が遅いので、 esbuild-jest の利用を検討しましたが、トランスパイルに失敗することがありました。今回は原因に深入りせずに tsc でトランスパイルした js(dist配下) ファイルに対してjestを実行する形にしました。 概ね一般的な設定ですが、esbuild が対応している css import がJestでエラーするためモックしています。 jest.condig.js const path = require('path'); module.exports = { roots: ['<rootDir>/dist'], testMatch: ['**/__tests__/**/*.+(jsx|js)', '**/?(*.)+(spec|test).+(jsx|js)'], moduleNameMapper: { '^@/(.+)': '<rootDir>/dist/$1', // CSS Import をモック '\\.(css|less|scss|sass)$': path.resolve(__dirname, './dist/styleMock.js'), }, }; まとめ 上記の通り色々とTIPSはあるものの、十分扱い易い形で開発環境を構築することができました。esbuildを利用して開発環境を構築したことで、ある程度のコード量になっても10秒もかからずビルドができてしまうので、実際に開発速度はかなり向上しました。今のところどうしても扱いにくいという点は見つかっていないのが現状です。 esbuild はすでに多くのツールに組み込まれ今後の発展が一層期待されますので、適宜開発環境もupdateしていければと考えています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactの環境をシェルスクリプト一発で作る

作ろうとしたきっかけ ちょっとテストしたいだけの時に度々環境を準備してあげないといけないのめんどくさい、、、 →スクリプトで一発で作れたら楽じゃん!! どうやって作るの?? シェルスクリプトを使って、1回コマンドを叩けばReactの環境を構築できるようにする スクリプト自体は至ってシンプル。 create-react-appを使って簡単に構築する create-react-env.shのようなスクリプトファイルを作成する 以下のスクリプトを記述 yarn init -y # スクリプトを実行する際に第一引数でアプリ名をつける npx create-react-app $1 zsh create-react-env.sh アプリ名を実行(bashの場合はzsh → bashに変えて実行する) $ zsh create-react-env.sh hoge yarn init v1.22.17 warning The yes flag has been set. This will automatically answer yes to all questions, which may have security implications. warning package.json: No license field success Saved package.json ✨ Done in 0.03s. Creating a new React app in /Users/morinagamasahiro/react-memos. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts with cra-template... ~~~ (中略) ~~~ Created git commit. Success! Created react-memos at /Users/morinagamasahiro/react-memos Inside that directory, you can run several commands: yarn start Starts the development server. yarn build Bundles the app into static files for production. yarn test Starts the test runner. yarn eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd react-memos yarn start Happy hacking! 無事完成。めでたしめでたし。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript x React x Storybook のプロジェクトを CSF 3.0 対応させようとして型問題でテンパったら読む記事

はじめに 先日 Storybook 6.4 がリリースされ、CSF 3.0 がフィーチャーフラグなしで使えるようになりました。 意気揚々と移行を試みたものの、story object に適用できる型がかなり増えており、どれを使うのが正解か理解するのに時間がかかりました。 同じように悩んでいる方の助けになればと思い、備忘録として調べた内容を残しておきます。 TL;DR Storybook 6.4 であることが前提ですが、とりあえず CSF 3.0 なら ComponentStoryObj, CSF 2.0 なら ComponentStoryFn 使っておけば間違いないでしょう。 Meta と Story --Storybook 6.2 以前の story object の型定義 Storybook 6.2 における story の書き方を振り返ってみましょう。 ※ フォーマットは CSF 2.0 に準拠しているので、storiesOf API 使ってる方の参考にはならないかもしれません。ごめんなさい。 // SomeComponent.stories.tsx import { Meta, Story } from "@Storybook/react"; import React from "react"; import { SomeComponent, SomeComponentProps } from ".."; const meta: Meta = { title: "SomeComponent", component: SomeComponent, }; export default meta; export const MyStory: Story<SomeComponentProps> = (args) => ( <SomeComponent {...args} /> ); MyStory.args = { // 省略 }; ここで登場する型は Meta と Story の 2 つのみです。型情報は以下から確認できます。 https://github.com/storybookjs/Storybook/blob/v6.2.9/app/react/src/client/preview/types-6-0.ts ComponentMeta と ComponentStory --Storybook 6.3 で登場した新しい型 Storybook 6.3 から新たに ComponentMeta, ComponentStory という型が登場しました。 これは既存の Meta, Story と何が違うのでしょうか?ライブラリが提供している型ファイルを見てみましょう。 https://github.com/storybookjs/Storybook/blob/v6.3.0/app/react/src/client/preview/types-6-3.ts // types-6.3.d.ts /** * For the common case where a component's stories are simple components that receives args as props: * * ```tsx * export default { ... } as ComponentMeta<typeof Button>; * ``` */ export declare type ComponentMeta< T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any> > = Meta<ComponentProps<T>>; /** * For the common case where a story is a simple component that receives args as props: * * ```tsx * const Template: ComponentStory<typeof Button> = (args) => <Button {...args} /> * ``` */ export declare type ComponentStory< T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any> > = Story<ComponentProps<T>>; ComponentMeta は Meta のエイリアス、ComponentStory は Story のエイリアスであることがわかります。 また ComponentMeta, ComponentStory を使う場合は型引数が必須となり、この時渡せるのは story のベースとなるコンポーネントの型のみです。 これらの新しい型の登場によって、 Storybook のためだけにコンポーネントの props を export する必要がなくなりました。 ちょうどこの issue への回答で ComponentMeta, ComponentStory という型を追加した意図が説明されています。 ただまあ、実はこんな風に書けばコンポーネントの props を export せずに済んではいたのですが…… Story<React.ComponentProps<typeof SomeComponent>> だとしてもやっぱりこっちの方がスッキリしてて嬉しいですよね! ComponentStory<typeof SomeComponent> Storybook 6.4 での変更 Storybook 6.4 から、CSF 3.0 をデフォルトでサポートするようになりました。これによって型ファイルにも大幅な変更が加えられています。 これから型ファイルの中身を見ていきますが、長くなるので先にまとめておくと、storybook 6.4 で登場する型は全部でこんな感じです。 えらい多いですね。 Meta これまでの Meta と同じ。 ComponentMeta Meta 型のエイリアス。 Storybook 6.3 の ComponentMeta と同じ。 StoryFn CSF 2.0 向けの型。詳細後述。 ComponentStoryFn StoryFn のエイリアス。型引数に story のベースとなるコンポーネントの型を入れて使う。 StoryObj CSF 3.0 向けの型。詳細後述。 ComponentStoryObj StoryObj のエイリアス。型引数に story のベースとなるコンポーネントの型を入れて使う。 Story 今のところは StoryFn のエイリアス。 Storybook 7.0 では StoryObj のエイリアスに変更される。 ComponentStory Story のエイリアス。型引数に story のベースとなるコンポーネントの型を入れて使う。 types-6.3.d.ts にアップデートが入り、 ComponentXX 系の型が増えています。まずはそちらから見ていきましょう。 ComponentXX 系の型の扱い方 結論から言うと ComponentXX 系の型はすべて XX という型のエイリアスです。 ComponentMeta, ComponentStory の時と同様、型引数に story のベースとなるコンポーネントの型を渡して使います。 https://github.com/storybookjs/Storybook/blob/v6.4.0/app/react/src/client/preview/types-6-3.ts どちらを使っても構いませんが、個人的には書き心地がスッキリする ComponentXX 型の方がいいなと感じています。 続いて新しく追加された StoryObj, StoryFn 等の型について確認していきます。 で結局どの型使えばいいの?についてですが、これは story の構成と CSF のバージョンによって変わってきます。 順に見ていきましょう。 Storybook 6.4 x CSF 2.0 の場合 結論: ComponentMeta と ComponentStoryFn がおすすめ 最初に貼ったコンポーネントの story のコード例の一部を再掲します。 Storybook のバージョンを上げても、CSF のバージョン移行をしていなければ、基本的に story のコードは変更なしでも動きます。 しかしながら、参照している型の内容には変化が生じています。 // SomeComponent.stories.tsx // Story 型は StoryFn のエイリアスとなった const MyStory: Story<SomeComponentProps> = (args) => ( <SomeComponent {...args} /> ); // ので、こう書いてもよい const MyStory: StoryFn<SomeComponentProps> = (args) => ( <SomeComponent {...args} /> ); // こう書くこともできる const MyStory: ComponentStory<typeof SomeComponent> = (args) => ( <SomeComponent {...args} /> ); // ということは、こう書いても良いことになる const MyStory: ComponentStoryFn<typeof SomeComponent> = (args) => ( <SomeComponent {...args} /> ); 型情報 https://github.com/storybookjs/Storybook/blob/v6.4.0/app/react/src/client/preview/types-6-0.ts https://github.com/storybookjs/Storybook/blob/v6.4.0/app/react/src/client/preview/types-6-3.ts 上記のどの書き方でも今は問題なく動くのですが、型ファイル中に NOTE that in Storybook 7.0, this type will be renamed to StoryFn and replaced by the current StoryObj type. とあるように、 Storybook 7.0 にアップデートされた時点で Story 型は StoryFn ではなく StoryObj のエイリアスとなります。 少しわかりにくいですが、 Storybook の migration guide にもそのように書いてあります。 よって、 特に理由がなければ StoryFn 型を採用する方がよいでしょう。 前述の通り、コードがスッキリするので ComponentStoryFn の方が個人的には好みです。 Storybook 6.4 x CSF 3.0 の場合 結論: ComponentMeta と ComponentStoryObj がおすすめ CSF 3.0 から story の記述方法がガラっと変わります。当初のサンプルコードを CSF 3.0 流に書き直すとこんな感じになります。 // SomeComponent.stories.tsx import SomeComponent from ".."; export default { component: SomeComponent }; export const Sample = { args: { // 省略 }, }; CSF 2.0 では関数だった story がオブジェクトになりました。ここまで来ればもう StoryFn と StoryObj の使い分けに迷うことはなさそうです。 ではここに型をあてていきます。型がなくてもエラーにならない場合もありますが、型推論が有効になった方が何かと嬉しいですしね。 // SomeComponent.stories.tsx import SomeComponent from ".."; const meta: ComponentMeta<typeof SomeComponent> = { component: SomeComponent }; export default Meta; // 公式ドキュメントでは type assertion を使って以下のようにするのが推奨されていたのですが、 // export default { component: SomeComponent } as ComponentMeta<typeof SomeComponent>; // 個人的に type assertion は型エラーを握り潰してしまう可能性があるのであまり使いたくなく、上記のように書き直しています。 export const Sample: ComponentStoryObj<typeof SomeComponent> = { args: { // 省略 }, }; Storybook 7.0 以降は Story 型が StoryObj のエイリアスとなるので、ほんの少し短く書けるようになります。 3 文字短縮されるだけなのでまあ、どちらでもいいと言えばいいですね。 // SomeComponent.stories.tsx for Storybook 7.0 export const Sample: ComponentStory<typeof SomeComponent> = { args: { // 省略 }, }; まとめ ドキュメントにはっきりとした記載がなかったので、 Storybook のバージョンを上げ下げして型ファイルを確認したり CSF のドキュメントを睨みつけたりして結構大変だった気がしてたんですが、こうしてまとめてみると案外シンプルな話でした。 ComponentXX 型は XX 型のエイリアスであり、型引数にコンポーネントの型をそのまま渡せるところが便利 Storybook 6.4 から Story 型は StoryFn のエイリアスとなった でも Storybook 7.0 以降の Story 型は StoryObj のエイリアスに変わるので、バージョンアップのタイミングで直しておきましょう ↑ とはいえ、それ直すくらいなら早めに CSF 3.0 対応済ませちゃった方がいいと思います 今年はこんな感じで大丈夫か? 大丈夫だ、問題ない ありがとうございました!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmコマンドやnpxコマンドでFetchError(socket hang up)が発生した

Reactの開発環境を作成しようと思ってnpx create-react-app react-sampleとコマンド入力したところ、エラー(FetchError reason: socket hang up)が発生しました。 また、他にもnpm install -g npm@[バージョン]等を実行した時にも同様のエラーが発生しました。 結局大した話ではなかったのですが、一応解決方法を残しておきます。 実際に発生したエラー 66 verbose stack FetchError: request to https://registry.npmjs.org/psl/-/psl-1.8.0.tgz failed, reason: socket hang up 66 verbose stack at ClientRequest.<anonymous> (C:\Program Files\nodejs\node_modules\npm\node_modules\minipass-fetch\lib\index.js:110:14) 66 verbose stack at ClientRequest.emit (node:events:390:28) 66 verbose stack at TLSSocket.socketOnEnd (node:_http_client:471:9) 66 verbose stack at TLSSocket.emit (node:events:402:35) 66 verbose stack at endReadableNT (node:internal/streams/readable:1343:12) 66 verbose stack at processTicksAndRejections (node:internal/process/task_queues:83:21) ~~~中略~~~ 74 error network request to https://registry.npmjs.org/psl/-/psl-1.8.0.tgz failed, reason: socket hang up 75 error network This is a problem related to network connectivity. 75 error network In most cases you are behind a proxy or have bad network settings. 75 error network 75 error network If you are behind a proxy, please make sure that the 75 error network 'proxy' config is set properly. See: 'npm help config' エラーの原因 ネットワークが原因でファイルのフェッチに失敗しているのは分かるのですが、別にプロキシ環境で実行しているわけでもないし、ネットワークが不調というわけでもない。なんで??? と思って上記エラーの数行上を見てみたら、こんなことが書いてありました。 57 silly audit error Beginning October 4, 2021, all connections to npm websites and the npm registry - including for package installation - must use HTTPS and TLS 1.2 or higher. 57 silly audit error 57 silly audit error For more information see this blog post: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/ 要するに2021年10月4日以降はレジストリサイトに接続するのに「HTTPS and TLS 1.2 or higher」を絶対使ってね☆ということでした。 現状の設定がどうなっているかはnpm config listで確認できました。結果、httpsではなくhttpを使用していることが判明しました。 D:\dev>npm config list ~~~中略~~~ http-proxy = null https-proxy = null msvs_version = "2015" proxy = null python = "python2.7" registry = "http://registry.npmjs.org/" ←ここ strict-ssl = false そして私がコマンドを実行したのは2021年12月2日。だからエラーになったということみたいです。 解決方法 以下のコマンドを実行してレジストリにアクセスする際にHTTPSを使うように変更しました。 npm config set registry https://registry.npmjs.org/ その後npx create-react-app react-sampleを実行したところ今度は成功しました。 終わり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmコマンドやnpx create-react-appコマンドでFetchErrorが発生した

Reactの開発環境を作成しようと思ってnpx create-react-app react-sampleとコマンド入力したところ、エラー(FetchError)が発生しました。 また、他にもnpm install -g npm@7.11.2を実行した時にも同様のエラーが発生しました。 結局大した話ではなかったのですが、一応解決方法を残しておきます。 実際に発生したエラー 66 verbose stack FetchError: request to https://registry.npmjs.org/psl/-/psl-1.8.0.tgz failed, reason: socket hang up 66 verbose stack at ClientRequest.<anonymous> (C:\Program Files\nodejs\node_modules\npm\node_modules\minipass-fetch\lib\index.js:110:14) 66 verbose stack at ClientRequest.emit (node:events:390:28) 66 verbose stack at TLSSocket.socketOnEnd (node:_http_client:471:9) 66 verbose stack at TLSSocket.emit (node:events:402:35) 66 verbose stack at endReadableNT (node:internal/streams/readable:1343:12) 66 verbose stack at processTicksAndRejections (node:internal/process/task_queues:83:21) ~~~中略~~~ 74 error network request to https://registry.npmjs.org/psl/-/psl-1.8.0.tgz failed, reason: socket hang up 75 error network This is a problem related to network connectivity. 75 error network In most cases you are behind a proxy or have bad network settings. 75 error network 75 error network If you are behind a proxy, please make sure that the 75 error network 'proxy' config is set properly. See: 'npm help config' エラーの原因 ネットワークが原因でファイルのフェッチに失敗しているのは分かるのですが、別にプロキシ環境で実行しているわけでもないし、ネットワークが不調というわけでもない。なんで??? と思って上記エラーの数行上を見てみたら、こんなことが書いてありました。 57 silly audit error Beginning October 4, 2021, all connections to npm websites and the npm registry - including for package installation - must use HTTPS and TLS 1.2 or higher. 57 silly audit error 57 silly audit error For more information see this blog post: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/ 要するに2021年10月4日以降はレジストリサイトに接続するのに「HTTPS and TLS 1.2 or higher」を絶対使ってね☆ということでした。 現状の設定がどうなっているかはnpm config listで確認できました。結果、httpsではなくhttpを使用していることが判明しました。 D:\dev>npm config list ~~~中略~~~ http-proxy = null https-proxy = null msvs_version = "2015" proxy = null python = "python2.7" registry = "http://registry.npmjs.org/" ←ここ strict-ssl = false そして私がコマンドを実行したのは2021年12月2日。だからエラーになったということみたいです。 解決方法 以下のコマンドを実行してレジストリにアクセスする際にHTTPSを使うように変更しました。 npm config set registry https://registry.npmjs.org/ その後npx create-react-app react-sampleを実行したところ今度は成功しました。 終わり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmコマンドやnpx create-react-appコマンドでFetchError(socket hang up)が発生した

Reactの開発環境を作成しようと思ってnpx create-react-app react-sampleとコマンド入力したところ、エラー(FetchError reason: socket hang up)が発生しました。 また、他にもnpm install -g npm@[バージョン]等を実行した時にも同様のエラーが発生しました。 結局大した話ではなかったのですが、一応解決方法を残しておきます。 実際に発生したエラー 66 verbose stack FetchError: request to https://registry.npmjs.org/psl/-/psl-1.8.0.tgz failed, reason: socket hang up 66 verbose stack at ClientRequest.<anonymous> (C:\Program Files\nodejs\node_modules\npm\node_modules\minipass-fetch\lib\index.js:110:14) 66 verbose stack at ClientRequest.emit (node:events:390:28) 66 verbose stack at TLSSocket.socketOnEnd (node:_http_client:471:9) 66 verbose stack at TLSSocket.emit (node:events:402:35) 66 verbose stack at endReadableNT (node:internal/streams/readable:1343:12) 66 verbose stack at processTicksAndRejections (node:internal/process/task_queues:83:21) ~~~中略~~~ 74 error network request to https://registry.npmjs.org/psl/-/psl-1.8.0.tgz failed, reason: socket hang up 75 error network This is a problem related to network connectivity. 75 error network In most cases you are behind a proxy or have bad network settings. 75 error network 75 error network If you are behind a proxy, please make sure that the 75 error network 'proxy' config is set properly. See: 'npm help config' エラーの原因 ネットワークが原因でファイルのフェッチに失敗しているのは分かるのですが、別にプロキシ環境で実行しているわけでもないし、ネットワークが不調というわけでもない。なんで??? と思って上記エラーの数行上を見てみたら、こんなことが書いてありました。 57 silly audit error Beginning October 4, 2021, all connections to npm websites and the npm registry - including for package installation - must use HTTPS and TLS 1.2 or higher. 57 silly audit error 57 silly audit error For more information see this blog post: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/ 要するに2021年10月4日以降はレジストリサイトに接続するのに「HTTPS and TLS 1.2 or higher」を絶対使ってね☆ということでした。 現状の設定がどうなっているかはnpm config listで確認できました。結果、httpsではなくhttpを使用していることが判明しました。 D:\dev>npm config list ~~~中略~~~ http-proxy = null https-proxy = null msvs_version = "2015" proxy = null python = "python2.7" registry = "http://registry.npmjs.org/" ←ここ strict-ssl = false そして私がコマンドを実行したのは2021年12月2日。だからエラーになったということみたいです。 解決方法 以下のコマンドを実行してレジストリにアクセスする際にHTTPSを使うように変更しました。 npm config set registry https://registry.npmjs.org/ その後npx create-react-app react-sampleを実行したところ今度は成功しました。 終わり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reduxの全体像をざっくり理解する奴

?全体像をざっくりを理解する 登場人物 Store あらゆる状態を管理している倉庫のような奴 Reducer Storeにある状態を更新する奴 Storeにある状態を更新できるのはReducerだけ 倉庫(Store)の中にいる仕分け人のようなイメージ Reducerが状態を更新するときは、現在のstateとactionを受け取る actionに応じて新しい状態を返す dispatch Reducerに現在のstateとactionを届けるだけな奴 「こういう注文(action)来てます〜。現在のstate(状態)もお届けしました〜。 Reducerさん、後はよろしくです〜。」なイメージ action dispatchされる注文伝票みたいな奴 action(注文伝票)をReducerに届けることで、Reducerはactionに応じて新しい状態を返す 例えばカウントを制御するアプリケーションの場合、 「カウント増やして〜」とか「カウント減らして〜」などのactionがある state 状態のこと。Storeで管理されている。 全体の流れ ここではわかりやすいようにカウントを制御するだけのReactアプリケーションを想定します。 ユーザー「カウント+1ボタンポチッと」 現在のstate(count: 0)と action(カウント増やして~)を Storeにdispatch(送る)する Storeにいる仕分け人Reducerが 現在のstate(count: 0)とaction(カウント増やして~)を受け取る Reducer「えぇーと、現在のstate(count: 0)とactionが"カウント増やして〜"ね、OK! 」 Reducer「はい完了! state(count: 1)」 React「お!stateが更新された。よっしゃー画面を新しい状態に更新するぞー」 ユーザー「カウントが"1"になった〜」 ?Redux ToolKitで全体像を理解する アプリは単一のコンポーネントと単一のストアを持つ Reactアプリケーションが階層の一番上に単一のコンポーネントを持っているように、Storeも単一です。 1つのアプリケーションで状態を管理するStoreは1つ。 全てのstateを1つのStoreで管理し、必要なデータを適宜取り出せる stateをコンポーネントツリーの外部にあるStoreで持つイメージ アプリの状態は単一のStore内のオブジェクトツリーに保存される import React from 'react' import ReactDOM from 'react-dom' import App from './App' import { store } from './app/store' import { Provider } from 'react-redux' ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('app'), ) Slice Storeが倉庫だとしたらSliceは倉庫の中にある棚のようなイメージ アプリケーションで扱う状態に応じてSliceを切り分けることで、状態の管理がしやすくなる。 例えばcountを扱うSliceやtodoを扱うSliceなどに分けることができる createSliceは state,reducer,actionをまとめて作成する関数(便利!!) import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' const initialState = { value: 0, } // Sliceの作成 export const counterSlice = createSlice({ name: 'counter', // actionの名前の一部になる initialState, // stateの初期状態 reducers: { increment: (state) => { // 状態を更新する関数(actionの名前の一部になる) state.value += 1 }, decrement: (state) => { state.value -= 1 }, // 非同期処理を扱うときに使用する extrareducer: ... }) // コンポーネントからactionをdispatchできるようにexport export const { increment, decrement } = counterSlice.actions // コンポーネントからstateを参照するための関数をexport export const selectCount = (state) => state.counter.value // Storeに登録するためにexport export default counterSlice.reducer state stateはinitialState に記述する const initialState = { value: 0, } export const counterSlice = createSlice({ //省略 initialState,  //省略 }) こう書いてもOK export const counterSlice = createSlice({ //省略 initialState: { value: 0, }  //省略 }) reducer reducersの中に状態を変更する関数をまとめる incrementとdecrementがreducerにあたる export const counterSlice = createSlice({ //省略 reducers: { increment: (state) => { state.value += 1 }, decrement: (state) => { state.value -= 1 }, //省略 }) action actionの名前はname/reducerというような形になる(Redux DevToolsでみると分かりやすい) 下記で言うとcounter/incrementのような形になる ただし、Redux ToolKitが内部でよしなに色々とやってくれるので、reducersに登録したreducerの名前を下記のようにexportしてあげればOK exportしたactionをコンポーネントからdispatchすることで状態が更新される export const counterSlice = createSlice({ name: 'counter', reducers: { increment: (state) => { state.value += 1 }, decrement: (state) => { state.value -= 1 }, }) // コンポーネントからactionをdispatchできるようにexport export const { increment, decrement } = counterSlice.actions SliceをStoreへ結合 Storeに登録するReducerのkeyでstateにアクセスできる Reducerのキーがcounterなのでstate.counterでアクセスできる import { configureStore } from '@reduxjs/toolkit' import counterReducer from '../features/counter/counterSlice' export const store = configureStore({ reducer: { counter: counterReducer, }, }) 状態の参照(useSelector) useSelector...stateに変更があったら自動的に再実行され、コンポーネントを再描画する import { useSelector } from 'react-redux' import { selectCount } from '../features/counter/counterSlice' const count = useSelector(selectCount) const Counter = () => { return( <> <button>クリック!!</button> <span>{count}</span> </> ) } 状態の更新(useDispatch) useDispatchはdispatch関数を返す actionを引数にdispatch関数を実行することでReducerが作動し、stateが更新される actionに引数を渡したらReducerでaction.payloadとして値を受け取れる import { useDispatch, useSelector } from 'react-redux' import { selectCount, increment } from '../features/counter/counterSlice' const dispatch = useDispatch() const count = useSelector(selectCount) const Counter = () => { return( <> <button onClick={()=> dispatch(increment())}>クリック!!</button> <span>{count}</span> </> ) } ?全体の流れを簡単におさらい アプリは単一のコンポーネントと単一のストアを持つ Sliceを作成してStoreへ結合する useSelectorを使用して状態の参照ができる useDispatchを使用して状態の更新ができる Tips はるか昔、actionを作るのに色々とごちゃごちゃしていましたが、 actionの作成に関してあまり意識しなくても良くなりました。 Redux ToolKitが中でいい感じに仕事してくれてると思って頂ければ良いと思います。興味がある方はググって見て下さい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hook Formで数値を処理する際にハマったこと

本記事は、React Advent Calendar 2021の6日目の記事です。 Reactでフォームを作成するとき、簡単に作成できるReact Hook Formを使うことがよくあるかと思います。 本記事では、私がReact Hook Formで数値を処理する際にハマったこととその解決方法を紹介します。 なお、記事中のコードは一部分の引用です。記事の最後にCodeSandboxのリンクを記載しています。 はじめに React Hook Formでは下記のようにuseForm()を使って必要な関数や値を取得します。 また、フォーム送信のハンドラ(ここではonSubmit)を定義して引数dataを与えることで、フォームに入力された値をdataで取得して、フォーム送信時の処理を記載することができます。 const App = () => { const { control, handleSubmit } = useForm(); const onSubmit = (data) => { // フォーム送信時の処理を記載する console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> {/* ここにフォームを作る */} <input type="submit" /> </form> ); }; TypeScriptを使う場合、useForm()に型を与えてdataの型を定義することができます。 この場合はIFormInputという型を定義しています。 interface IFormInput { firstName: string; lastName: string; } const App = () => { const { control, handleSubmit } = useForm<IFormInput>(); // 型を定義 const onSubmit = (data: IFormInput) => { console.log(data); }; さらに、React Hook FormはMUI(Material-UI)と組み合わせて使うこともできます。 ここでは、MUIのTextFieldと組み合わせて利用してみます。 作成したフォームはこちらです。 コードはこちらです。 下記のように、Controllerコンポーネントを用いることで、React Hook FormとMUIと組み合わせて使うことができます。 interface IFormInput { firstName: string; lastName: string; } const App = () => { const { control, handleSubmit } = useForm<IFormInput>(); const onSubmit = (data: IFormInput) => { console.log(data); }; // Controllerコンポーネントを用いることでMaterial-UIと組み合わせて利用可能 return ( <form onSubmit={handleSubmit(onSubmit)}> <Stack spacing={2}> <label>First Name</label> <Controller render={({ field }) => <TextField {...field} />} name="firstName" defaultValue="" control={control} /> <label>Last Name</label> <Controller render={({ field }) => <TextField {...field} />} name="lastName" defaultValue="" control={control} /> <input type="submit" /> </Stack> </form> ); }; より詳細な使い方は本記事では省略させていただきますので、公式ドキュメントなどをご参照ください。 発生した問題 今回、私はフォームで金額を入力したかったため、IFormInputにnumberのpriceを追加しました。 また、priceをカンマ区切りで表示するためにtoLocaleString()を使いました。 type FormData = { firstName: string; lastName: string; price: number; // 追加 }; const App = () => { const { control, handleSubmit } = useForm<IFormInput>(); const onSubmit = (data: IFormInput) => { console.log(data.price.toLocaleString()); // カンマ区切りで表示したい return ( <form onSubmit={handleSubmit(onSubmit)}> <Stack spacing={2}> <label>First Name</label> <Controller render={({ field }) => <TextField {...field} />} name="firstName" defaultValue="" control={control} /> <label>Last Name</label> <Controller render={({ field }) => <TextField {...field} />} name="lastName" defaultValue="" control={control} /> {/* 追加 */} <label>Price</label> <Controller render={({ field }) => <TextField {...field} />} name="price" defaultValue="" control={control} /> <input type="submit" /> </Stack> </form> ); }; しかし、100000を入力してもコンソールには100000と出力され、うまく動きませんでした。(期待値は100,000) 原因を調査したところ、typeofでpriceの型を出力すると原因が分かりました。 interface IFormInput { firstName: string; lastName: string; price: number; } const App = () => { const { control, handleSubmit } = useForm<IFormInput>(); const onSubmit = (data: IFormInput) => { console.log(typeof data.price); // string なんとpriceはstringになっていました。 なので、toLocaleString()を使ってもカンマ区切りにはならなかったのです。 IFormInputでnumberを指定してすっかり安心してしまっていました。 フォームからの出力なのでよく考えればstringしか定義できないような気はしますが、numberで指定して特にエラーにもならず、VSCodeの型表示もnumberとなっていたこともあり、ハマってしまいました。 異なる型でも代入できるのは、TypeScriptの型付けの闇な気がします。 修正版 priceはstringにして、キャストして使うように修正しました。 interface IFormInput { firstName: string; lastName: string; price: string; } const App = () => { const { control, handleSubmit } = useForm<IFormInput>(); const onSubmit = (data: IFormInput) => { console.log(Number(data.price).toLocaleString()); // カンマ区切りで表示される }; 全体のコードはこちらをご確認ください。 数値以外を入力できないようにバリデーションを追加しています。 以上になります。 最後までお読みいただきありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでアプリを作成しました【10】【Web フォーム ②】

参考サイト 入門者でもわかるReact Routerを利用したルーティング設定の基礎 【React TypeScript】react -router-domの導入!Material UIのボタンを使って画面遷移をしよう! Reactでアプリを作成しました【6】【Web フォーム ①】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでアプリを作成しました【10】【マルチステップの入力フォーム ②】

【要件】 1. 基本情報入力画面 (1) 性別と生年月日を入力できる (2) 『進む』ボタン 2. アンケート画面 (1) 最初に表示されている設問は1つのみ (2) 設問に答えると次の設問が表示される (3) 『戻る』ボタンと『進む』ボタン 3. 相談内容入力画面 (1) Textareaに自由に入力できる (2) 『戻る』ボタンと『進む』ボタン (3) 『進む』ボタンは動作しなくても良い 4. 確認画面 (1) 入力した内容が表示される (2) 『戻る』ボタンと『送信』ボタン (3) 『送信』ボタンは動作しなくても良い Reactアプリの作成する $ npx create-react-app <アプリ名> 必要なパッケージのインストール Material-UI を利用するため、事前にライブラリをインストールする。 // with npm $ npm install @mui/material @emotion/react @emotion/styled // with yarn $ yarn add @mui/material @emotion/react @emotion/styled $ npm install react-router-dom ソースコード RouteCSSTransitionGroupを実際に使ってみたソースです。 JSの構成は以下のようになっていて、app.jsをbrowserifyでバンドルしています。 src/js ├── app.js ├── components │ ├── App.js │ ├── RouteCSSTransitionGroup.js │ └── pages │ ├── Basic.js │ ├── Questionnaire.js │ └── Optional.js │ └── confirm.js │ └── routes.js 参考サイト 入門者でもわかるReact Routerを利用したルーティング設定の基礎 【React TypeScript】react -router-domの導入!Material UIのボタンを使って画面遷移をしよう! Reactでアプリを作成しました【7】【マルチステップの入力フォーム】 react-routerとreact-router-domの違い react-routerでページ遷移にちょっとしたアニメーションを付ける
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

useReducerとuseContextでglobal stateを作る場合のおすすめなやり方

はじめに React Contextでglobal stateを実現する際、useStateやuseReducerを使ってcontextの伝搬で更新するやり方が知られていますが、contextを一つだけにすると、無駄なレンダリングが生じることがあります。そこで、contextの伝搬を使う場合は、contextを細かく分けることがおすすめです。 コード 最初にuseReducerでstateを作るカスタムフックを作ります。 const useValue = () => useReducer((state, action) => { if (action.type === 'INC_COUNT1') { return { ...state, count1: state.count1 + 1 }; } if (action.type === 'INC_COUNT2') { return { ...state, count2: state.count2 + 1 }; } if (action.type === 'INC_BOTH_COUNTS') { return { ...state, count1: state.count1 + 1, count2: state.count2 + 1 }; } throw new Error('unknown action type: ' + action.type) }, { count1: 0, count2: 0 }) 次にそれぞれの項目を伝搬させるためのcontextを作ります。 const DispatchContext = createContext(() => {}); const Count1Context = createContext(0); const Count2Context = createContext(0); Providerコンポーネントは次のようになります。入れ子にならないようにreduceRightを使います。 const Provider = ({ children }) => { const [{ count1, count2 }, dispatch] = useValue(); return [ [DispatchContext.Provider, { value: dispatch }], [Count1Context.Provider, { value: count1 }], [Count2Context.Provider, { value: count2 }], ].reduceRight( (a, c) => createElement(c[0], c[1], a), children ); }; 実際にstateを使う2つのコンポーネントがこちらです。2つともほぼ同じです。 const Counter1 = () => { const count1 = useContext(Count1Context); const dispatch = useContext(DispatchContext); const inc = () => dispatch({ type: 'INC_COUNT1' }); return ( <div> Count1: {count1} <button onClick={inc}>+1</button> </div> ); }; const Counter2 = () => { const count2 = useContext(Count2Context); const dispatch = useContext(DispatchContext); const inc = () => dispatch({ type: 'INC_COUNT2' }); return ( <div> Count2: {count2} <button onClick={inc}>+1</button> </div> ); }; 最後に全体をまとめるコンポーネントです。 const App = () => ( <Provider> <div> <Counter1 /> <Counter1 /> <Counter2 /> <Counter2 /> </div> </Provider> ); CodeSandbox 課題 今回はシンプルな例でしたが、オブジェクトの構造が複雑でも、必要な数だけcontextを作ればなんとかなります。しかし、contextは動的には作れない(作ることはできるが、全部再マウントすることになってしまう)ので、あらかじめ分かっている固定数のものにしか対応できません。 おわりに これをすんなり使おうと思える方はぜひどうぞ。こんなことするくらいならライブラリを使おうと思う方は、これとかこれとかこれとかをどうぞ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「毎日誰かのプルリクを脳死でマージするアドベントカレンダー」の乗っかってみた

以下のアドベントカレンダーの2日目です。 Repositoryはこちらですね! あまりnext.jsの知見がないのですが、 完全にきっかけ作りで参加です。 ハードル下がればいいなと思ってあまりきちんとしたコードは書いてませんが、 とりあえずPR出しました! やったこととして contributorの追加 contributorへのリンクをトップページに加える prettierを入れてみる です。 今後の方に期待しつつ、このRepositoryをwatchしていきます! 3日目は @XxGodmoonxX さんです。よろしくお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

結局のところ、E2Eとフロントエンドテスト(Jest Integration Test)のどちらでテストを書いたらいいのか

TL;DR Jest Integration Test でやったほうが良さそうなこと First Fetch(ページの初回表示)で完結するもの Mutation系(#post, #patch, #delete)はE2Eが適切そう ユーザー操作が伴うが、Requestが発生しないもの バリデーション ボタンのDisabled属性 ドラッグアンドドロップ パターンが複数ある場合は、1つはE2Eで行い、残りのバリエーションはJest側で担保すると良い 本題はここからになります ? この記事の対象とするテスト なぜテストを書くのかを整理する 安心して開発するためにも、どんどんテスト書いていきたい バグが減らないので、ソフトウェアごと吹き飛ばしてやる そもそもなぜテストを書くとよいのか 欠陥が減る 機能要件を満たすことを確認できる などがあり、それらのおかげでソフトウェアとしての信頼性がテストにより向上し、 欠陥が検出できるので、安心して開発できる テストケースによって、仕様の理解が進む などの効果が得られます また、 欠陥が生じた時に検出できる安心感が生まることで安心して開発できる コンテキストや仕様の理解がスムーズに進むなど といったメリットがあります この記事の対象とするテスト 次にこの記事で対象としているテストについては ユニットテスト custom Hookなどの関数のテスト 結合テスト コンポーネント単位のテスト ページ単位のテスト E2Eテスト ソフトウェア全体のテスト ユニットテストの重要性はいろいろな記事で言及されているので、 結合テストとE2Eどちらのテストを書くのがいいのか についてに焦点を当てています この記事の環境 { "react": "^17.0.2", "jest": "^27.3.1", "@testing-library/jest-dom": "^5.15.1", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", "msw": "^0.35.0", } 用語の定義 Jest Integration Test Jest で行うフロントエンドの、ページ単位の結合テストです ページ単位のテスト∽コンポーネント単位の結合テストになっています E2E End to Endで行うソフトウェア全体のテストです Jest Integration Test と E2E のどちらで書くのか 心構え 一般的には、信頼性はE2Eの方が高く、コストはIntegration Testに軍配が上がります そのためトレードオフによる選択になるのでチームで合意が取れればどちらでもいいと思います また、テストはないよりあるほうがいいので、迷った時は書きやすいほうで書くで十分だと思います また参考の画像は実際の開発している画面になります なるべくトロフィーに近い形を目指して 引用: the-testing-trophy-and-testing-classifications Jest Integration Testを基準に分類しています そのため、Jest Integration TestでやらないものをE2Eでやるとよいと思います RECOMMEND(おすすめ) Jest Integration Test で行った方がよいもの 初回のFetchで終わるもの 初回のページ表示やユーザー操作が行われないもの。 下記画像では権限や、ステータスによって表示の切り替えがある部分をテストしています。 初回表示画面 通常パターンのテーブルのTest expect(within(preRegistrations[0]).getByRole('cell', { name: '適用待ち' })).toBeVisible() expect(within(preRegistrations[0]).getByRole('cell', { name: '2030/12/01' })).toBeVisible() expect(within(preRegistrations[0]).getByRole('cell', { name: '昇給者' })).toBeVisible() expect(within(preRegistrations[0]).getByRole('cell', { name: '従業員(568名)' })).toBeVisible() expect(within(preRegistrations[0]).getByRole('cell', { name: '2030/12/08' })).toBeVisible() バリエーションとして、Statusによって切り替わるケースTest userEvent.click(within(preRegistrations[3]).getByRole('button', { name: '予約を操作' })) userEvent.hover(screen.getByTestId('pre-registration-dropdown-menu-link-delete-info-icon')) expect(within(preRegistrations[3]).getByRole('cell', { name: '適用済み' })).toBeVisible() expect(screen.getByRole('tooltip', { name: '削除可能期限を過ぎているため削除できません。' })).toBeVisible() 他にも ページネーションの表示/非表示の切り替え Query Parameterが含まれる直叩きのURLによって表示が切り替わる部分 などをテストしています 動的に表示が変わるレイアウト コンポーネント側で担保したりすることが多いです 初回のfetch と内容はほとんど一緒ですが、テストケースを考えるときに静的なレイアウトと混同し、忘れられがちなので明記してます 入力によって文言が変わるインフォメーションパネル(昇級者の部分が変わる)画面 タイトルのTest expect(screen.getByRole('region', { name: '【昇給者】のダウンロードを受け付けました。' })).toBeVisible() 他にも フラッシュメッセージ 数値をカンマ区切りにして表示する などをテストしています API連携せずにフロントエンドで完結するもの 初回のFetchを除いて、ユーザーが操作があるけどRequestが発生しない部分をテストします。 編集Dialogのバリデーションとボタンの属性(バリエーションエラーの時、更新ボタンはDisabled属性になる)画面 バリデーションのTest input.setSelectionRange(0, 3) userEvent.type(input, '{backspace}') expect(screen.getByText('予約名を入力してください。')).toBeVisible() expect(screen.getByRole('button', { name: '更新' })).toBeDisabled() userEvent.type(input, 'あ'.repeat(31)) expect(screen.getByText('予約名は30文字以内で入力してください。')).toBeVisible() expect(screen.getByRole('button', { name: '更新' })).toBeDisabled() 他にも ダイアログの表示、非表示 ドラックアンドドロップ などをテストしています OPTIONAL(任意) Jest Integration Test でやったほうがいいけど、そこまで重要度が高くないものになります どこまでやるかは、開発者やQAで合意が取れてればよいと思います 静的なレイアウト テーブルのヘッダーや、ダイアログのタイトルなどがこれに当たります どこまで文言をテストするのか、それらをテストする効果はどれくらいあるのか、などは曖昧ですが翻訳などが関わるのであればテストで変更箇所を検知できるといいと思います また、テストをするのであれば、コンポーネント単位で行うのが適切だと思います テーブルのヘッダーのTest const th = rows[0] expect(within(th).getByText('ステータス')).toBeVisible() expect(within(th).getByText('適用日')).toBeVisible() expect(within(th).getByText('予約名')).toBeVisible() expect(within(th).getByText('対象')).toBeVisible() expect(within(th).getByText('作成者')).toBeVisible() アクセシビリティ(Role) ユーザーのソフトウェアの利用に近い形でテストすることで、アクセシビリティを最低限テストできます 具体的にはReact Testing Libraryを持ちいた、Elementの取得する際に*ByRoleメソッドを使うことで、Roleの確認が行えます 要素の取得方法の優先順位についてはドキュメントにあります https://testing-library.com/docs/queries/about#priority ローディングなどのUIロジック ローディングやユーザーイベントに起因するUI変更などのUIロジックのテストです ただ、テストしづらいので、マニュアルテストで担保するでも十分だと思います ページ内ローディング→絞り込み実行→テーブル内ローディング ユーザーがイベントを実行すると、表示位置が変わる(window.scrollTo(0,0)) ユーザーがイベントを実行すると、URL.searchParamsが変わる などをテストしています 400系 E2Eでは発生させずらいネットワークエラー(HTTP Status: 400)のテストをすることで、フロントエンドのバグや、UIのテストをすることが可能になります 今の環境では、MSWというネットワーク層インターセプトをするMocking Libraryを使っているので、それを用いた400系のテストをしています 予約のダウンロードに失敗するTest // APIをMockでテストしてる server.use(getPreRegistrationsExportExpect400) await screen.findByText('予約管理') const preRegistration = getPreRegistrations()[0] userEvent.click(within(preRegistration).getByRole('button', { name: 'ダウンロード' })) expect(within(await screen.findByRole('alert')).getByText('予約のダウンロードに失敗しました。')).toBeVisible() Mockの中身 export const getPreRegistrationsExportExpect400 = rest.get(`${path.api.pre_registrations.export.mock}`, (_, res, ctx) => { return res( ctx.status(400), ctx.json({ message: 'ダウンロードに失敗しました。', }), ) }) APIを叩いた時のRequest BodyやQuery Parameter ここでInputによって変わるRequest Bodyの中身やQuery Paramterが正しく渡されているかを担保するテストを書きます これらは実装の詳細テストになり、E2E側でも担保されるものが適切なので任意になっています ただ、リファクタリング等をするのであればテストを書いた方が信頼感、安心感は高くなると思います 名前の編集のTest const input: any = screen.getByLabelText('予約名') input.setSelectionRange(0, 3) userEvent.type(input, '{backspace}昇格者降格者') userEvent.click(screen.getByRole('button', { name: '更新' })) await expectSecondFetch() expect(within(await screen.findByRole('alert')).getByText('予約名を更新しました。')).toBeVisible() const targetsRequest = listenerRequest.mock.calls .flat() .filter((it) => matchEndpoint(it.url.pathname) && it.method === 'PATCH') expect(targetsRequest[0].body).toEqual({ name: '昇格者降格者', }) listenerRequestはmock関数になっており、MSW側でcatchされた内容を格納しています const listenerRequest = jest.fn<MockedRequest, MockedRequest[]>() server.on('request:match', (req) => { listenerRequest(req) }) FAQ E2E と Jest Integration Test どっちでもテストを書けるんだけど最適なのはどっち?? E2Eではソフトウェア全体で正しく動いてるのか確認するためにテストを1つ書いて、Jest Integration Test でバリエーションを網羅するように書くとポートフォリオのバランスがよくなります E2Eに比べてJest Integration Testが書きづらい テストはないよりはある方が確実によいのでチーム内で合意が取れていれば、E2Eでも問題ないと思っています Jest Integration Testを追加することに越したことはないんですが、各々のテスト環境にそれぞれ癖があると思うので、無理する必要はないと思います テストのいい感じの書き方を教えて欲しい describeは対象や条件、itは期待する結果 を書きます it は仕様書に結びつくとメンテがしやすくなるので機能要件をもとに書くとよいです また、it == Assertion(Expect) になるので、Assertionが多すぎるのであればテストを分割してもよいです そのため、1つのテストに複数のAssertionが含まれるのは問題ないです コンテンツが表示され、ページネーションが非表示になる it('コンテンツが表示され、ページネーションが非表示になる', () => { // assert: コンテンツが表示される const preRegistrations = rows.slice(1) expect(within(preRegistrations[0]).getByRole('cell', { name: '適用待ち' })).toBeVisible() ... // assert: ページネーション が非表示 expect(screen.queryByLabelText('ページネーション')).not.toBeInTheDocument() }) またフロントエンドのテストは肥大化しやすいので、AAAパターンを使うとわかりやすくなります AAAパターンとは、Arrange(テストのための準備)、Act(処理の実行)・Assert(結果の確認)の三つのブロックに分けて書く方法です コンテンツの削除に成功し、インフォメーションパネルとフラッシュメッセージが表示されるTest it('コンテンツの削除に成功し、インフォメーションパネルとフラッシュメッセージが表示される', () => { // Arrange(テストのための準備) const table = screen.getByRole('table') const deleteButtonInTable = table.getByRole('button', { name: '削除' }) // Act(処理の実行) userEvent.click(deleteButtonInTable) // Assert(結果の確認) // expectはインフォメーションパネルが表示されること expect(within(await screen.findByRole('alert')).getByText('昇給者の削除を受け付けました。')).toBeVisible() expect(screen.getByRole('region', { name: '【昇給者】の削除を受け付けました。' })).toBeVisible() // Arrange const link = screen.getByRole('link', { name: 'バックグラウンド処理' }) // Assert // expectはフラッシュメッセージが表示されること expect(link).toBeVisible() expect(link).toHaveAttribute('href', '/admin/job_histories') }) AAA Paternの書き方は以下の記事が詳しいです frontend-testing-pitfalls 最後に テストを書く、布教する際に大事にしてる気持ち
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptと仲良くなる(初心者向け)

 TypeScriptと仲良くなる はじめまして、初投稿です。 私は約半年前から現場でReact、TypeScriptを扱うプロジェクトにアサインしています。 当時は、1行改修するにも丸一日かかるようなチュートリアルレベルでした、、 そんな半年前の自分に向けてTypeScriptで躓いた構文をまとめたいと思います。 若干ボリューミーに見えますが、簡単なコードが多いだけですw ① typeの書き方 まずは基本 engineer.ts type Engineer = { name: string age: number sex: string languages: string[] }; const engineer: Engineer = { name: 'ホゲ', age: 25, sex: 'man', languages: ['TypeScript', 'React', 'Java'], }; typeはオブジェクトの型を定義しています。 typeを定義しなくてもオブジェクトは問題なく作れるのですが、 各valueに好きな値を入れられてしまうため、いつの間にか変テコなオブジェクトになっていて なんかしらんけどバグってる。ってことになりかねません。 値の不正代入も防げて、IDEの入力補完も効くので型無しのJavaScriptよりも開発効率が良いと個人的に思います。 ② オブジェクトからtypeを作る engineer.ts const Sex = { man: '男', woman: '女', }; type SexType = keyof typeof Sex; type Engineer = { name: string age: number sex: SexType languages: string[] }; この構文も初めて見たときは何が起きているのかさっぱりでした。 結論としてはsexに入れられる値をmanまたはwomanの文字列だけに制限しています 1つずつ解説 const Sex = { man: '男', woman: '女', }; constで定義されているのでこれ単体だとただのオブジェクトです。 type SexType = keyof typeof Sex; 見慣れない構文keyof、typeofですが、一つずつ見ると typeof Sex; /*     ↓オブジェクトの構造がそのままtypeになる { man: '男', woman: '女', }   */ keyof typeof Sex; /*     ↓typeofでtypeになったSexのkey(文字列)のみを持つtypeになる 'man' | 'woman' */ 文字で説明すると逆にややこしい気がしますが、、 type Engineer = { name: string age: number sex: SexType /* 'man' | 'woman' */ languages: string[] }; const engineer: Engineer = { name: 'ホゲ', age: 25, sex: 'weman' /* 怒られる */, languages: ['TypeScript', 'React', 'Java'], }; もともとstringだったところをSexTypeに置き換えると、 man, woman以外の文字列を入れようとするとコンパイルエラーになります。 コンパイルエラーになる以外にも、 sexがmanだったときに男を画面に表示することもとても楽になります。 console.log(engineer.sex); /* man */ console.log(Sex[engineer.sex]); /* 男 */ 同じようにLunguageTypeを作るのもいいですね ③ 任意の型を追加する Engineerのプロフィールが寂しいので追加で hobbyとfavoriteFoodを任意で設定出来るようにしましょう type Engineer = { name: string age: number sex: SexType languages: LanguageType[] otherInfo: { hobby: string | undefined favoriteFood: string | undefined } }; type Engineer = { name: string age: number sex: SexType languages: LanguageType[] otherInfo: { hobby?: string favoriteFood?: string } }; type OtherInfo = { hobby: string favoriteFood: string }; type Engineer = { name: string age: number sex: SexType languages: LanguageType[] otherInfo: Partial<OtherInfo> }; 3通り書きましたが、typeとしてはすべて同じ内容になります。 3番めのPartial<>ですが、これはUtility Typesの一つで使いこなせると便利なやつです。 詳しくははこちらの方の記事がおすすめです。 【TypeScript】Utility Typesをまとめて理解する ④ typeを繋げる type Name = { name: string; } type Age = { age: number } type Engineer = Name & Age & { sex: SexType languages: LanguageType[] otherInfo: Partial<OtherInfo> }; こんな感じで細かくtypeを定義して&でつなげてひとまとめにできます。 もちろんName、Age以外も同じように定義しておいてすべて&でつなげることも可能です。 おまけ   BrandedType type Engineer = { firstName: string lastName: string age: number sex: SexType languages: LanguageType[] }; nameだったところをfirstNameとlastNameに分ける事になった場合、 どちらも型としてはstringで区別することはできません。 type FirstName = string & { _brand: 'FirstName' }; type LastName = string & { _brand: 'LastName' }; type Engineer = { firstName: FirstName lastName: LastName age: number sex: SexType languages: LanguageType[] }; stringに{ _brand: (型名等) }という他と被らないプロパティを付けることで、区別出来るようになります。 BrandedTypeは自身で把握出来る程度の規模であれば無くてもいいのかなと思いますが、 細部まで型が定められていると、参照が増えたり複雑な構造になった際にもミスを防げるし、自分以外の人が見たときにも分かり易いんじゃないかなと思います。 まとめ 今ではどれも当たり前のように使えるようになりましたが、 当時は本当に分からなくて頭に定着するまで何度も調べていました、、笑 TypeScriptで書くと、開発効率が良くなるのはもちろんですが、他の人が見たときに分かりやすいのもメリットだと思います。← とても大事 (頑張ります) 奥が深くまだまだ知らないこともあるので引き続き頑張ります。 最後まで見ていただきありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】【Vue】 など流行りのフレームワークを使うもののためのオブジェクト指向とは

流行りのフレームワークを使っていると忘れがち(意識しなくてよい)JSの基礎を学び直してみた。 オブジェクト指向とは 直訳するとオブジェクトはモノ。しかしこれではあまりに分かりにくいですよね。 要約すると「現実世界のものに見立ててプログラミングをする方法」になります。 これでも想像つきにくいですよね。つまりこのオブジェクトを使って、わかりやすくプログラミングするよというふわっとした意味らしいです。 ※ { name:"菅田将暉"; age:28; job:"actor" } そして、このオブジェクトを作る方法が2パターンあります。 ★オブジェクトリテラル リテラルというのは文字通りという意味です。これはいつも何気なく使っている書き方です。 const person = { name:"菅田将暉", age:28, job:"actor" } ★クラス クラスというのは ・オブジェクトの設計図 ・クラスから作られたオブジェクトはインスタンスと呼ばれる とりあえず見ていきましょう。 例 class Person { constructor(initName,initAge,initAge) { this.name = initName, this.age = initAge, this.job = initJob } } こういった形でクラスを定義します。 この設計図を使うにはnewというものを用いて、 const masaki = new Person("菅田将暉",28,"actor") と書くと、※で示したオブジェクト同じものが出来上がります。 この設計図からオブジェクトを作ることをインスタンス化と呼び、このmasakiがインスタンスと呼ばれます。(インスタンスという名前は付きましたがオブジェクトです。) 同じもの作るのに、なぜクラスなんか使わなくてはいけないの?と思った方もいるかもしてません。 理由は簡単、クラスは何回も使いまわしできるからです。設計図と呼ばれる所以かもしれません 例えば、 const nana = new Person("小松奈菜",25,"actress") const Mackenyu = new Person("新田 真剣佑",25,"actor") という新しいインスタンスも、簡単に作れてしまうわけです。いまは、オブジェクトのプロパティ(オブジェクトの中の情報)が3つしかないので、そのまま書いてもさほど大変ではありませんが、これが10個もあったりしたら、オブジェクトを3つ作るだけでも、大変ですよね。設計図を書いておけば、情報さえ渡してやれば、いくつも短い記述量でオブジェクトが作れるようになります。 この記事でクラス、インスタンス、コンストラクタあたりを詳しく書いたので、参考にしてみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

美男美女+ゆりやんレトリィバァを使って解説するオブジェクトとは

流行りのフレームワークを使っていると忘れがち(意識しなくてよい)JSの基礎を学び直してみた。 オブジェクト指向とは 直訳するとオブジェクトはモノ。しかしこれではあまりに分かりにくいですよね。 要約すると「現実世界のものに見立ててプログラミングをする方法」になります。 これでも想像つきにくいですよね。つまりこのオブジェクトを使って、わかりやすくプログラミングするよというふわっとした意味らしいです。 ※ { name:"菅田将暉"; age:28; job:"actor" } そして、このオブジェクトを作る方法が2パターンあります。 ★オブジェクトリテラル リテラルというのは文字通りという意味です。これはいつも何気なく使っている書き方です。 const person = { name:"菅田将暉", age:28, job:"actor" } ★クラス クラスというのは ・オブジェクトの設計図 ・クラスから作られたオブジェクトはインスタンスと呼ばれる とりあえず見ていきましょう。 例 class Person { constructor(initName,initAge,initJob) { this.name = initName, this.age = initAge, this.job = initJob } } こういった形でクラスを定義します。 この設計図を使うにはnewというものを用いて、 const masaki = new Person("菅田将暉",28,"actor") と書くと、※で示したオブジェクト同じものが出来上がります。 この設計図からオブジェクトを作ることをインスタンス化と呼び、このmasakiがインスタンスと呼ばれます。(インスタンスという名前は付きましたがオブジェクトです。) 同じもの作るのに、なぜクラスなんか使わなくてはいけないの?と思った方もいるかもしてません。 理由は簡単、クラスは何回も使いまわしできるからです。設計図と呼ばれる所以かもしれません 例えば、 const nana = new Person("小松奈菜",25,"actress") const Mackenyu = new Person("新田 真剣佑",25,"actor") という新しいインスタンスも、簡単に作れてしまうわけです。いまは、オブジェクトのプロパティ(オブジェクトの中の情報)が3つしかないので、そのまま書いてもさほど大変ではありませんが、これが10個もあったりしたら、オブジェクトを3つ作るだけでも、大変ですよね。設計図を書いておけば、情報さえ渡してやれば、いくつも短い記述量でオブジェクトが作れるようになります。 この記事でクラス、インスタンス、コンストラクタあたりを詳しく書いたので、参考にしてみてください。 ・クラスの継承 例えば、芸人さんの設計図 [class Comedian]を作るとします。 class Comedian { constructor(initName,initAge,initJob,initCompany) { this.name = initName, this.age = initAge, this.job = initJob, this.company = initCompany } } これでもクラスは作ることができますが、同じようなことを二回書くのはナンセンスですよね。 そこでクラスには、継承という機能があります。 class Comedian extends Person{ } これでPersonクラスを継承できました。 class Comedian extends Person{ } const suda = new Comedian("菅田将暉",28,"actor") こう書けば、※と同じオブジェクトができます。(クラスは芸人さんですが。) このPersonクラスを継承して所属事務所を付けたすと class Comedian extends Person{ constructor(initName,initAge,initJob,initCompany) { // 親クラスのコンストラクタを呼び出す super(initName,initAge,initAge,); this.company = initCompany } } こうかく そしてインスタンス化すると const yuriyann = new Comedian("ゆりやんレトリィバァ",31,"comedian","吉本興業") と書くことができます。 これは const yuriyan = { name: "ゆりやんレトリィバァ", age: 31, job: "comedian", company: "吉本興業", }; というオブジェクトと同じものが出来上がります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのState管理を比較してみた (useState / useReducer / React Context API / Redux / Recoil)

おはこんばんちは、@ちーずです。 アドベントカレンダー2日目はReactのステート管理についてです。 Reactでは、いろんな方法でステート管理できますね。 React hooksでは、useStateやuseReducer、useContextなど... さらにstate管理のライブラリも、ReduxやMobX、recoilなど色々あって何がなんだかわからない... そう感じている人もいると思います。(※ 自分です) それらをどのように使うことができるか、どんな時に使うと良いかなどをまとめてみました!! React hooksでのステート管理 1. useState 一番基本的なState管理ができるhooksです。 // const [state変数, stateの更新関数] = useState(初期値) const [state, setState] = useState(initialState) 2. useReducer useState同様、State管理ができるhooksです。 // reducer関数 // const reducer = (1つ前のState, action) => State // actionは基本、type と payload を持っている const reducer = (state, action) => { switch (action.type) { case "ACTION_NAME": return { ...state, newState } default: return state; } } // const [state変数, reducerの実行関数] = useReducer(reducer関数, 初期値) const [state, dispatch] = useReducer(reducer, initialState) useStateに比べて、少々複雑に感じますね。 シンプルなステート管理はuseStateで事足りるのですが、 下記のようなケースはuseReducerの方が適していると言われています。 複数の値を操作する必要がある複雑なロジック 前のstateに基づいて次のstateを操作したい時 詳しくみていきましょう! 事例1. 複数の値を操作するuseReducer 今回は、名前と電話番号を入力するフォームを想定して実装してみます。 import { useReducer, Reducer } from "react"; // Stateの型 type State = { name: string; telNum: string; }; // Actionの型 type Action = { type: "UPDATE_NAME" | "UPDATE_TEL"; payload: string; }; // reducer const reducer: Reducer<State, Action> = (state, action) => { switch (action.type) { case "UPDATE_NAME": return { ...state, name: action.payload }; case "UPDATE_TEL": return { ...state, tel: action.payload }; default: return state; } }; // 初期値の設定 const initialState = { name: "", telNum: "" }; const App = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <> <div> <label>名前: </label> <input value={state.name} // dispatchを使ってreducerにactionを送る onChange={(e) => { dispatch({ type: "UPDATE_NAME", payload: e.target.value }); }} /> </div> ... </> ); }; useStateで記述する場合、stateを別々で扱う必要がありますが、 useReducerを使うことで1つのstateにすることができます。 また、useReducerの重要な特徴として、reducerがstateに依存した関数ではないこと、 つまり非依存な純粋関数です。 そのため、テストの記述のしやすい、可読性があがる、などのメリットがあります。 事例2. useReducerを使って記述を短くできるケース useReducerは前のstateに基づいて次のstateが決まる処理がお得意です。 そのため、toggleを実現するには非常に相性が良いです。 // useStateの場合 const [isOpen, setIsOpen] = useState(false) const toggleIsOpen = () => setIsOpen(!isOpen); // useReducerの場合 const [isOpen, toggleIsOpen] = useReducer((prev) => !prev, false); `useReducerを使うと1行でかけちゃいましたね!! useReducerを使ってパフォーマンス改善?! useReducerのdispatch関数はメモ化されます。 そのため、React.memoを組み合わせて使うことでパフォーマンスを最適化することができます。 ▼ 参考 しかし、useState, useReducerはローカルステートであり、 Propsしない限り、単一のコンポーネントの中でしかstateを扱うことができません。 そこで登場するのがReact Contextです。 3. React Context API React Context APIは、propsのバケツリレーをせずとも、 コンポーネントの階層に関係なくstateを子コンポーネントに伝えることができるAPIです。 import { createContext, useState, useContext } from 'react'; // 1. Contextの作成 // (Contextはstateを管理する箱、Storeのようなものと考えればok) export const Context = createContext(undefined); // 2. Context.Providerコンポーネントの作成 // Providerは子コンポーネントにstateを配布できるようにする役割 // 渡したいstateやstate更新関数をvalueに渡すことで、Contextに値を流し込むことができる export const ContextProvider = ({ children }) => { const [state, setState] = useState(''); return ( <Context.Provider value={[state, setState]}> {children} </Context.Provider> ); }; // 3. Contextに格納された値ををhooksで取得する // Consumerと役割は同様 export const useStateContext = () => { return useContext() } 最初は書き方に慣れないかもしれませんが、 このようにとても簡単にグローバルなステート管理が実現できます。 また、useReducerとReact Contextを組み合わせて使えば、 後述するReduxに近しいデータストア設計を作成することもできます。 しかし、React Contextはいくつかのデメリットがあります。 React Contextは複数作ることができるため、データフローの整備が難しい ちゃんとメモ化しないと、再レンダリング地獄になる可能性あり... Providerの値が更新される度に、useContextを利用しているコンポーネントはすべて再レンダリングされるため ▼ 参考 ライブラリを利用したステート管理 個人的には基本的にReact Hooksが提供してくれているState管理で十分かな、と思っていますが、 世の中にはState管理ができる様々なライブラリがあります。 ライブラリを用いいると、データストアを外部に持たせたり、 データフローのグラフ化やロギングなどよりリッチなState管理ができるものが多いです。 その中から特に人気のライブラリを軽くご紹介します! 1. Redux まずはState管理ライブラリの王といっても過言ではない、Reduxをご紹介します。 ReduxはReact Context APIと異なりコンポーネントツリーの外部に1つのデータストアを持たせて Stateの管理ができるライブラリです。 様々なメソッドや管理ツール(Redux Toolkit)が提供されており、 より柔軟にState管理ができます。 ▼ 公式ドキュメント ▼ 参考 アプリケーション全体のStateを一つの場所で管理することができるため、データフローを整備しやすいですが、 一方で全体のStateをオブジェクトで管理しており、常に更新し続けるような設計になっています。 そのため、ちょっとしたコードのミスでStateが一気に消失してしまう可能性も... そこで生まれたのがRecoilです。 2. Recoil Recoilはコンポーネントツリーの外部にAtomというデータストアを複数のを持たせてStateの管理ができるライブラリです。 本家Facebookによって公開されたState管理ライブラリであり、今ちょっとアツめです。(※ 主観です) (正直自分も使ったことがない、詳しくないため、別途調査して追記していきます...!) ▼ 公式ドキュメント ▼ 参考 結局どれを選べばいいの...? 正直本当にケースバイケースだな、という印象です。 記事を通して筆者が考えた使い分けは以下の通りです! (まだ初学者よりであるため、ご意見いただけるとありがたいです!!) シンプルなローカルステート(プリミティブ型、一次元)を扱う → useState 複雑なローカルステート(オブジェクト)を扱う → useReducer ローカルステート & 前のstateに基づいて次のstateを操作したい時 → useReducer グローバルなステート管理をしたい → React Context API 大規模プロジェクトでデータフローを整備しながらグローバルなステート管理をしたい → ライブラリを用いる 1つのデータストアで管理したい → Redux 複数のデータストアで管理したい → Recoil また、様々な観点から比較してみた記事もたくさんあるので、 いろんな情報を参考にしてみて、サービスにあったState管理を探してみるのがよいのかな?と思いました! 以上、ReactのState管理についてでした!! 明日はuseEffectのお話をします
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

引越し業界初の新サービスを支えるフロントエンドの技術

この記事はエイチーム引越し侍 / エイチームコネクトの社員による、Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021 2日目の記事です。 「引越し侍 ネット見積もりサービス」 のβ版をリリースしました 昨日、 @sho-hata が 「引越し業界初の新サービスを支えるバックエンド技術」というのを投稿してくれたので、 であれば、フロントエンドも、、ということで @anneauがフロントについてお話させていただきます。 結構、チャレンジングな技術構成してるので、皆さんの参考になる部分もあるかと思い、 イケてるところとイケてないと思ってるところを書こうと思います。 技術構成 まず、全体の技術構成は図を見ていただければと思います。 画像作成: @sho-hata フロントエンドに関係のある部分のみ解説します Next.js フロントエンドの構築にはNext.jsを採用しました。 Next.jsの詳細は不要かと思いますので、内部構成のみお伝えします。 SSRで実行 データの取得はGraphQL GraphQLクライアントにはReact Relayを採用 ユーザー認証にはFirebase Authentication リアルタイム性が問われるチャットにはFirestore Hasura あまり聞き慣れないかと思いますが、HasuraはGraphQL エンジンという役割を担っております。 図のように、クライアントでGraphQLのクエリを定義し、Hasuraにリクエストを飛ばせば、自動的にSQLを作成し、データベースにクエリを走らせてくれます。 Firebase ユーザー認証やチャットなどリアルタイム性が問われるところには、Firebaseを用いています。 Hasuraとも相性がよく、jwtでFirebase側のユーザー認証情報をHasura側で解釈することができます イケてるところ Next.js 採用による色々 普段開発してるとその恩恵が当たり前になりがちですが、 DX向上 Fast Refresh TypeScript Support UX向上 Image Optimization その他 SSR Support API Routes 等々、Vercel teamの皆さん、本当にお世話になっています Hasura採用によるバックエンド工数削減 Next.js → Hasuraの通信にはGraphQLを用いています。 GraphQLでは、クライアントが望むデータをクライアント側に定義していくようになります。 query { profile { id name avatar } } 本来であれば、GraphQLサーバーの方で、このクエリを解釈し、SQLを発行する必要がありますが、 上で述べたとおり、これらは全てHasuraが担ってくれます。 認証やパフォーマンスチューニングもHasuraがやってくれるため、基本的にバックエンドが不要となります。 GraphQLでは解決できないような複雑な処理は HasuraからRest APIを呼び出せるようになっています。 僕たちのサービスでは、暗号化を必要とする部分や複雑なビジネスロジックはGolangに任せています React Relay × Hasura 採用による型の安全性 API側のスキーマとTypeScriptの型定義をあわせるのは労力のいることかと思いますが、 現状、ほぼ0工数で実現することができています。 GraphQLのクライアントには、React Relayを用いており、 Hasuraが出力してくれるGraphQLスキーマをRelay Compilerにかませることで、 バックエンドとフロントエンドの型に一貫性をもたせることができています。 Nx採用によるモノレポ化 Nxというのはフロントエンド特化のモノレポ管理ツールです。 モノレポとは? 複数のプロジェクトを一つのリポジトリで管理しようという概念で GoogleやFacebook、Uberなど多くの企業が採用しているアーキテクチャです。 リソースの共通化 linterやbuilderの共通化 コンポーネントや処理の共通化 ビルドスピード向上 変更を加えていない部分はキャッシュすることでビルドスピードを上げる 依存関係の見える化 などメリットが多くあります。 例) ユーザー、提携企業、社内を一つのリポジトリで管理 このように一つのリポジトリで構成することでリソースの共通化がしやすくなります。 イケてないと思ってるところ GraphQLによるフロントエンドのコード肥大化 GraphQLはクライアントから自由にデータを取り出せるというメリットがある一方、 パース処理などをクライアント側でする必要があります。 例) TODOコンポーネント const STATUS = { 1: '未着手', 2: '着手中', 3: '完了', }; const query = graphql` query todoQuery on Todo { title status deadline } ` const Todo: VFC = () => { const data = useQuery(query); return ( <> <p>{data.title}</p> <p>{STATUS[data.status]}</p> <p>{dayjs(data.deadline).format('YYYY-MM-DD')}</p> </> ); } バックエンドを介していれば、 ステータスはバックエンド側で変換して、表示用に日本語をもらう 日付はフォーマットされたものを返してもらう ということができますが、現状、これらがフロントの責務となってしまっています。 Firebaseの処理をクライアントが担っていること 構成図を見てもらえればわかるかと思いますが、 現状、フロントから発生する通信がHasuraに一本化できておらず、Firebaseとも通信をしております できる限り保守しやすい状態は保とうとしておりますが、フロントエンドが担っている責務が大きすぎるような気がしています。 今後の展望 やりたいことは色々ありますが、まずはイケてないと思っているところで取り上げた、 「フロントエンドの肥大化」というところは解消していきたいと思っています。 世の中的にはBFFによる解決策が多いかと思いますが、 僕たちの場合、HasuraがBFFに近い動きをしてくれていますし、Nextjsのapi routesも一部使用しているため、少し冗長な気もしています。 引き続き議論は進めていきますが、少なくともFirebaseの処理はサーバーサイドに寄せていきたいと思っています。 明日 Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021 2日目の記事は、いかがでしたでしょうか。 明日は @ex_SOUL の投稿になります!お楽しみに!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Material-UI + react-hooks-formで入力フォーム作成

React Material-UI + react-hooks-form React.js の入力フォームのライブラリに react-hooks-form がありますが、これと Material-UI のコンポーネントを組み合わせる方法についてまとめます。 検索に時間がかかったので備忘録がてらまとめます。 全体像 全体を Boxコンポーネント(Form 要素)で囲み、TextField を配置しボタンで Submit 処理を発火しています。 肝となるのは react-hook-form の Controller コンポーネントです。 Controller コンポーネントの Props に render がありますが、こちらに表示したい Material-UI のコンポーネントを配置します。今回は TextField ですが、CheckBox なども可能です。 // 1.FormすべてのPropsを宣言 // subject, email, messageのコードは省略している type FormProps = { name: string; gender: string; subject: string; email: string; message: string; }; const Contact: React.FC<any> = (props) => { // 2,react-hook-form使用の宣言 const { handleSubmit, control, formState: { errors }, } = useForm<FormProps>({ mode: "onBlur", // blur イベントからバリデーションがトリガーされます。 criteriaMode: "all", // all -> 発生した全てのエラーが収集されます。 shouldFocusError: false, //true -> エラーのある最初のフィールドがフォーカスされます。 }); // 3.Submit発火時に実行されるメソッド。ここでPOSTメソッドなどを呼ぶ const onSubmit: SubmitHandler<FormProps> = (data) => { console.log(data); }; return ( <Box component="form" marginTop="50px" width="100%" display="flex" flexDirection="column" justifyContent="center" onSubmit={handleSubmit(onSubmit)} > <Controller name="name" control={control} rules={{ required: "入力必須ですよ!", maxLength: { value: 30, message: "30文字以下で入力してくださいね!", }, }} render={({ field: { onChange, onBlur, value, name, ref }, fieldState: { invalid, isTouched, isDirty, error }, }) => ( <TextField label="お名前" placeholder="田中太郎" multiline required value={value} variant="outlined" margin="dense" onChange={onChange} onBlur={onBlur} error={Boolean(error)} helperText={error?.message} /> )} /> {/*/email, gender, subject, message は省略*/} <Button type="submit" color="primary" variant="contained" size="large"> 送信する </Button> </Box> ); }; Props の型 Props の型は以下の通りですが、これは入力フォームの持つ要素に応じて変更します。 お名前、件名、メールアドレス、お問い合わせ内容があるイメージです。 type FormProps = { name: string; gender: string; subject: string; email: string; message: string; }; useForm react-hook-form を使用するために必要な関数宣言です。 関数宣言の引数にオブジェクトとしてオプションを渡しています。API v6 const { handleSubmit, control, formState: { errors }, } = useForm<FormProps>({ mode: "onBlur", // blur イベントからバリデーションがトリガーされます。 criteriaMode: "all", // all -> 発生した全てのエラーが収集されます。 shouldFocusError: false, //true -> エラーのある最初のフィールドがフォーカスされます。 }); Controller 外部制御されたコンポーネントを使用するためのラッパーコンポーネントです。 公式によると Controller の Props である render の内部で Material-UI のコンポーネントを使用すると簡単になるらしいです。 Controller React Hook Form rules という Props にオブジェクトを渡すことで、バリデーションを設定できます。以下にあるのは未入力、31文字以上の際にエラーメッセージを表示させる例です。pattern など正規表現も取り扱っています。useForm register rules={{ required: "入力必須ですよ!", maxLength: { value: 30, message: "30文字以下で入力してくださいね!", }, }} render のコールバックの引数にある name や onChange は、react-hook-form からの値を Material-UI コンポーネントの TextField に渡しています。fieldState オブジェクトの error は、name 要素がエラーだった(上記の rules が True のとき)、エラーのステートを返却します。エラーメッセージは error.message で取得できるのでそれを helperText の Props に渡してバリデーションメッセージを表示しています。 <Controller name="name" control={control} rules={{ required: "入力必須やで!", maxLength: { value: 30, message: "30文字以下で入力するんやで!", }, }} render={({ field: { onChange, onBlur, value, name, ref }, fieldState: { invalid, isTouched, isDirty, error }, }) => ( <TextField label="お名前" placeholder="田中太郎" required value={value} variant="outlined" margin="dense" onChange={onChange} onBlur={onBlur} error={Boolean(error)} helperText={error?.message} /> )} /> 参考 React Hook Form によるシンプルなフォームバリデーション。 react-hook-form Controller MUI Text Field
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

linariaでReact/Next.jsのUIを構築する

React でのスタイリングをどうするかはなかなか考え処であり、チャレンジングな分野である。 最近フロントエンドで話題になっている linaria というライブラリを使ってみたので、紹介も兼ねてここに記す。 また、後半では React におけるコンポーネント開発の手段として storybook を使用する上で、linaria の導入方法にも触れる。 linariaとは フロントエンドにスタイルを適用するためのCSS in JSライブラリ。 styled-components のように、テンプレートリテラルの中にcssをそのまmあで記述するという方式をとっている。 css をほぼそのままコピペして突っ込むことが可能なため、デザイナーから渡される CSS を素早くプログラムに組み込める。 ビルド時に静的に CSS ファイルを生成し、実行時にそれを読み込むため、一般的に styled-components よりも高速だと言われている。 styled-components との比較 主に同じ CSS in JS ライブラリである styled-components との比較をしてみた。 一応どちらも触ったことがありなるべく公平を期したつもりだが、現プロジェクトで linaria を使用しているため、若干バイアスがかかっているかもしれない。 項目 linaria styled-components 表示速度 速い(と言われている) (linaria よりは)遅い(と言われている) CSS の生成タイミング ビルド時 実行時 導入コスト 少し高い1 低い Github スター数2 8.4k 35.3k dynamic styling △3 ○ それぞれの開発者が Github 上で意見を交わしている記事はこちら。 linaria 導入 前提 以下の項目を導入済みであること Next.js TypeScript インストール linaria パッケージのインストールを行う。 npm i --save @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker .babelrc に以下の記述をする。(まだない場合は作成する) .babelrc { "presets": ["next/babel", "linaria/babel"] } こちら のページが参考になる。 Next.js 用に設定する 以下のパッケージを導入する。 npm i --save next-linaria next.config.js にて以下の記述を行う。 next.config.js const withLinaria = require('next-linaria'); module.exports = withLinaria({}); これにより、ビルド時に Next.js により linaria を使用して CSS ファイルを生成できるようになる。 記述例 1. シンプルにボタン // components/Button.tsx にてコンポーネントを定義 import { styled } from '@linaria/react'; import React from 'react'; export const SimpleButton = ({ label, color, onClick }: { label: string; color?: string; onClick?: () => void }) => { return ( <StyledButton color={color} onClick={onClick}> {label} </StyledButton> ); }; const StyledButton = styled.span` background-color: ${(p) => p.color ?? '#59add9'}; border-radius: 16px; padding: 10px; :hover { background-color: ${(p) => p.color ?? '#3099cf'}; cursor: pointer; } `; // -------------------------------------------------- // pages/index.tsx にて呼び出し import {SimpleButton} from 'components/SimpleButton' import React from 'react'; const Button = () => { return (<SimpleButton label={"Simple Button"} />) } export default Button; ビルド npm run dev or npm run build 表示 ビルドが成功すると、.linaria-cache という隠しフォルダがプロジェクトルートに生成され、生成された CSS ファイルはその中に認められる。 以後、ソースコードが更新されるたびに、必要に応じて CSS ファイルが再生成される。(スタイルの記述に変更があった場合) ちなみに、ビルドに失敗したり、成功して画面が表示されてもうまくスタイルが適用されていないなどの問題がある場合は、以下のコマンドでキャッシュをクリアした後再度ビルドするとうまく行ったりする。 rm -rf .next .linaria-cache 表示速度比較 coming soon storybook で利用する storybook とは コンポーネントの開発サイクルを速める目的で導入される。 アプリ本体とは別でサーバーが起動され、UIも専用のものが立ち上がる。 そのUI上では、コンポーネントが受け取るパラメータを自由に変更し、即動作確認を行える。4 storybook のインストール npx sb init これにより、.storybook と src/stories という二つのフォルダが生成される。 こちら のページが参考になる。 stories/XXX.stories.tsx でビルド済み css をインポート stories/XXX.stories.tsx は storybook に配置するコンポーネントの呼び出しロジックのようなもの。 つまり、linaria により生成された CSS を storybook で読み込むには、その設定が必要である。 なぜなら、storybook は Next.js とは別のコンテキストで動作するため、ビルド済み CSS の読み込み設定(webpack)が別途必要だからである。 具体的には .storybook/main.js において以下のような記述を追加する。 main.js module.exports = { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: ['@storybook/addon-links', '@storybook/addon-essentials'], + webpackFinal: (config) => { + ... + // enable linaria's css to be loaded as it is modified + config.module.rules.push({ + test: /\.(tsx|ts|js|mjs|jsx)$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve('linaria/loader'), + options: { + sourceMap: process.env.NODE_ENV !== 'production', + ...(config.linaria || {}), + extension: '.linaria.module.css' + } + } + ] + }); + .... + return config; + } }; これは、Next.js での設定時に利用した next-linaria パッケージの index.js 内の webpack 設定箇所をほぼそのまま流用している。 storybook の起動 npm run storybook 上述の設定により、起動中のソースコード(スタイル部分)の修正がそのまま反映UIにされる。 所感 linaria と styled-components は、先に紹介した開発者の対談にあるように、どちらが優れているとかいう話ではないと思う。 また、React でのスタイルシステム構築には、CSS in JS 以外にも CSS モジュールやインライン方式、また従来のようにそのままクラス名を適用する、といったやり方がある。 実際、今回紹介した linaria と併用して、インラインスタイルなども結構書いていたりする。 都度その時に重要な指標は何かを見定め、ベターなチョイスを持って採用していくのが良いと感じている。 記載のように、Next.js で使用する場合や storybook を利用する場合は、webpack の設定が必要だったりして少し面倒 ↩ 2021/12/1時点 ↩ メディアクエリのようなものはサポートされていない。また IE ではそもそもサポートされていない。 ↩ 設定次第 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

next-pwa が動かないよー ウワァァァン

本記事の対象者 最近(2021年11月以降) React + Next + next-pwaライブラリを利用したPWAを作成しようとしていて 本番環境でService Workerの問題でPWAが動作しない現象に悩まされている という人。 要約 現時点(2021-12-02)のnext-pwaライブラリとそのライブラリが利用するworkboxライブラリにおいて、 デフォルト設定ではService Workerが正常動作しません。 next.config.js に以下のような除外設定を追加する必要があります。 buildExcludes: [/middleware-manifest.json$/] // 追加 現象の再現 1. プロジェクト新規作成 node v16.13.0 環境にて、 create-next-app + progressive-web-appテンプレートを使い、 プロジェクトを新規作成します。 ## create-next-app + progressive-web-appテンプレートで 新規プロジェクト my-app を作成 npx create-next-app --example progressive-web-app my-app ローカルの開発環境でdevサーバを立ち上げ、 ブラウザで http://localhost:3000/ を開くと PWAが正常動作する。 ## 開発環境で動作確認 cd my-app npm run dev 2. Vercelにデプロイ その後 Githubにpushして、Vercelにプロジェクトを取り込んでデプロイします。 (手順省略) しかし、デプロイされたページをブラウザで見てみると、PWAとしてアプリケーション保存ができません。 Chromeのデベロッパーツールで見てみると、以下の問題が目に付きます。 ChromeのPWAインストールボタンが表示されない Installabilityに問題あり No matching service worker detected. You may need to reload the page, or check that the scope of the service worker for the current page encloses the scope and start URL from the manifest. サービスワーカー関連の問題みたい。 Consoleエラー workbox-*.jsの処理にて、Uncaught (in promise) bad-precaching-response: bad-precaching-response :: [{"url":"https://.../_next/server/middleware-manifest.json","status":404}]というメッセージ。 middleware-manifest.json が見つからないっぽい。 何が問題か? 以下で議論されています。 next-pwaライブラリが利用するworkboxライブラリの間にミスマッチがあるか、またはworkboxの問題により、想定外のファイル(middleware-manifest.json)を読み込もうとしているのではないか、と推測しているようです。 next-pwa not working in Production Envrionment · Issue #295 · shadowwalker/next-pwa 対応 ワークアラウンドとして、next.config.js に buildExcludesオプションの記述を追加します。 直接の問題となっているファイル(middleware-manifest.json)を明示的に除外するオプションに指定しています。 next.config.js const withPWA = require('next-pwa') const runtimeCaching = require('next-pwa/cache') module.exports = withPWA({ pwa: { dest: 'public', runtimeCaching, buildExcludes: [/middleware-manifest.json$/], // 追加 }, }) 再度Vercelで確認すると、今度は正常にPWAアプリとしてインストールすることができるようになりました。 いずれnext-pwaライブラリ、またはworkboxライブラリで対応されると思われますが、今しばらくはこのワークアラウンド対応でしのぎましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web標準のみでReactのチュートリアルを再現

概要 Reactチュートリアル のマルバツゲームを、Web標準のみ外部ライブラリなしのVanilla JSで再現する。 実際に動いているものはこちら。 https://htsnul.github.io/vanilla_js_react_tutorial/ 1つずつ見ていこう。 Square Reactでの、 .square { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } と、 function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); } を、Vanilla JSでは、 class SquareComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> button { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } </style> <button></button> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelector("button").innerHTML = this.props.value || ""; this.root.querySelector("button").onclick = this.props.onClick; } } customElements.define("square-component", SquareComponent); とした。 基本的に要素をWeb Componentsを使って実装していく。 Web Componentsについては、 Web Components | MDN が分かりやすい。 最初の比較がReactの関数コンポーネントとの比較になってしまうので、 Vanilla JSではちょっと長くなってしまうように感じられるが、 後に出てくるクラスコンポーネントとの比較ではそこまで長くならない。 コンストラクタで雛形を作成し、 setProps (後には setState も)が呼ばれたら、update によって、 手動での差分更新を行う。 Shadow DOMにより、スタイルのスコープ化ができている。 そのため、これぐらいの規模のコンポーネントであれば、 CSSクラス名を使わずとも気にせずタグでスタイルが指定できる。 今後もだいたい同じ流れでVanilla JSにしていく。 props について、Web Componentの属性値で受け渡す手もあるのだが、 Web Componentの属性値は文字列しか扱えないため、 コールバックなどを扱うことを考えると、setProps など、 別関数に分けて別途呼び出すのが良いように思えた。 また、カスタム要素の名前をsquare-component と component を付加しているが、 これは、カスタム要素は、名前にハイフンが含まれる必要があるためである。 Board Reactでの、 .board-row:after { clear: both; content: ""; display: table; } と、 class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); } render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } を、Vanilla JSでは、 class BoardComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> .board-row:after { clear: both; content: ""; display: table; } </style> <div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> </div> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelectorAll("square-component").forEach((elm, i) => { elm.setProps({ value: this.props.squares[i], onClick: () => this.props.onClick(i) }); }); } } customElements.define("board-component", BoardComponent); とした。 今回はクラスコンポーネントなので比較しやすい。 Reactでは render で行っている処理を、 Vanilla JSではコンストラクタでの雛形作成と、update での手動での差分更新に分離している。 Game Reactでの、 .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } と、 class Game extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => this.handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); } } を、Vanilla JSでは、 class GameComponent extends HTMLElement { constructor() { super(); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> :host > div { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } </style> <div> <div> <board-component></board-component> </div> <div class="game-info"> <div></div> <ol></ol> </div> </div> `; this.update(); } setState(state) { this.state = { ...this.state, ...state }; this.update(); } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } update() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; const fragment = document.createRange().createContextualFragment(` <li> <button>${desc}</button> </li> `); fragment.querySelector("button").onclick = () => { this.jumpTo(move); } return fragment; }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } this.root.querySelector("board-component").setProps({ squares: current.squares, onClick: (i) => this.handleClick(i) }); this.root.querySelector(".game-info > div").innerHTML = status; this.root.querySelector(".game-info > ol").replaceChildren(...moves); } } customElements.define("game-component", GameComponent); とした。 handleClick や jumpTo は差がない。 コンストラクタや、render の大部分も同じである。 これらも、雛形を作って、手動での差分更新をするという部分が変更点になっている。 movesについては、DocumentFragment を生成して配列にして、 それを、replaceChildren で一気に入れ替えしている。 毎回DOMを上書きしてしまっているのでReactに比べると効率が悪くなってしまっているはずだ。 このあたりは、手動での差分更新を最小にするコストとの記述の簡潔さとのバランスになってきそうだ。 その他部分 その他の部分は、 function calculateWinner(squares) { // ... } があるが、これも完全に同じで変更なしで動く。 全体像 Web ComponentsによりCSSは各コンポーネントで指定できるので、 HTMLとJSのみで動作する。 index.htmlは、 <!DOCTYPE html> <script type="module" src="index.js"></script> <game-component></game-component> とだけ記載しておけばよい。 JSはここまでのを全体を載せると、 class SquareComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> button { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } </style> <button></button> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelector("button").innerHTML = this.props.value || ""; this.root.querySelector("button").onclick = this.props.onClick; } } customElements.define("square-component", SquareComponent); class BoardComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> .board-row:after { clear: both; content: ""; display: table; } </style> <div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> </div> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelectorAll("square-component").forEach((elm, i) => { elm.setProps({ value: this.props.squares[i], onClick: () => this.props.onClick(i) }); }); } } customElements.define("board-component", BoardComponent); class GameComponent extends HTMLElement { constructor() { super(); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> :host > div { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } </style> <div> <div> <board-component></board-component> </div> <div class="game-info"> <div></div> <ol></ol> </div> </div> `; this.update(); } setState(state) { this.state = { ...this.state, ...state }; this.update(); } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } update() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; const fragment = document.createRange().createContextualFragment(` <li> <button>${desc}</button> </li> `); fragment.querySelector("button").onclick = () => { this.jumpTo(move); } return fragment; }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } this.root.querySelector("board-component").setProps({ squares: current.squares, onClick: (i) => this.handleClick(i) }); this.root.querySelector(".game-info > div").innerHTML = status; this.root.querySelector(".game-info > ol").replaceChildren(...moves); } } customElements.define("game-component", GameComponent); function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; } となる。 実際に動いているものはここで確認可能だ。 https://htsnul.github.io/vanilla_js_react_tutorial/ まとめ Reactの見どころはDOMの差分適用が大きいが、 それ以外にも、コンポーネント化や、単方向データフローなどもある。 今回は、Vanilla JSで手動での差分更新は行っているが、 コンポーネント化や、単方向データフローは実現できている。 本チュートリアルの見どころのタイムトラベルなども実現できている。 手動での差分更新が許容できるならば、このような作り方もあり得るかもしれない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vanilla JSでReactのチュートリアルを再現

Vannila JSでReactのチュートリアルを再現 概要 Reactチュートリアル のマルバツゲームを、Web標準のみ外部ライブラリなしのVanilla JSで再現する。 実際に動いているものはこちら。 https://htsnul.github.io/vanilla_js_react_tutorial/ 1つずつ見ていこう。 Square Reactでの、 .square { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } と、 function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); } を、Vanilla JSでは、 class SquareComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> button { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } </style> <button></button> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelector("button").innerHTML = this.props.value || ""; this.root.querySelector("button").onclick = this.props.onClick; } } customElements.define("square-component", SquareComponent); とした。 基本的に要素をWeb Componentsを使って実装していく。 Web Componentsについては、 Web Components | MDN が分かりやすい。 最初の比較がReactの関数コンポーネントとの比較になってしまうので、 Vanilla JSではちょっと長くなってしまうように感じられるが、 後に出てくるクラスコンポーネントとの比較ではそこまで長くならない。 コンストラクタで雛形を作成し、 setProps (後には setState も)が呼ばれたら、update によって、 手動での差分更新を行う。 Shadow DOMにより、スタイルのスコープ化ができている。 そのため、これぐらいの規模のコンポーネントであれば、 CSSクラス名を使わずとも気にせずタグでスタイルが指定できる。 今後もだいたい同じ流れでVanilla JSにしていく。 props について、Web Componentの属性値で受け渡す手もあるのだが、 Web Componentの属性値は文字列しか扱えないため、 コールバックなどを扱うことを考えると、setProps など、 別関数に分けて別途呼び出すのが良いように思えた。 また、カスタム要素の名前をsquare-component と component を付加しているが、 これは、カスタム要素は、名前にハイフンが含まれる必要があるためである。 Board Reactでの、 .board-row:after { clear: both; content: ""; display: table; } と、 class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); } render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } を、Vanilla JSでは、 class BoardComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> .board-row:after { clear: both; content: ""; display: table; } </style> <div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> </div> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelectorAll("square-component").forEach((elm, i) => { elm.setProps({ value: this.props.squares[i], onClick: () => this.props.onClick(i) }); }); } } customElements.define("board-component", BoardComponent); とした。 今回はクラスコンポーネントなので比較しやすい。 Reactでは render で行っている処理を、 Vanilla JSではコンストラクタでの雛形作成と、update での手動での差分更新に分離している。 Game Reactでの、 .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } と、 class Game extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => this.handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); } } を、Vanilla JSでは、 class GameComponent extends HTMLElement { constructor() { super(); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> :host > div { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } </style> <div> <div> <board-component></board-component> </div> <div class="game-info"> <div></div> <ol></ol> </div> </div> `; this.update(); } setState(state) { this.state = { ...this.state, ...state }; this.update(); } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } update() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; const fragment = document.createRange().createContextualFragment(` <li> <button>${desc}</button> </li> `); fragment.querySelector("button").onclick = () => { this.jumpTo(move); } return fragment; }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } this.root.querySelector("board-component").setProps({ squares: current.squares, onClick: (i) => this.handleClick(i) }); this.root.querySelector(".game-info > div").innerHTML = status; this.root.querySelector(".game-info > ol").replaceChildren(...moves); } } customElements.define("game-component", GameComponent); とした。 handleClick や jumpTo は差がない。 コンストラクタや、render の大部分も同じである。 これらも、雛形を作って、手動での差分更新をするという部分が変更点になっている。 movesについては、DocumentFragment を生成して配列にして、 それを、replaceChildren で一気に入れ替えしている。 毎回DOMを上書きしてしまっているのでReactに比べると効率が悪くなってしまっているはずだ。 このあたりは、手動での差分更新を最小にするコストとの記述の簡潔さとのバランスになってきそうだ。 その他部分 その他の部分は、 function calculateWinner(squares) { // ... } があるが、これも完全に同じで変更なしで動く。 全体像 Web ComponentsによりCSSは各コンポーネントで指定できるので、 HTMLとJSのみで動作する。 index.htmlは、 <!DOCTYPE html> <script type="module" src="index.js"></script> <game-component></game-component> とだけ記載しておけばよい。 JSはここまでのを全体を載せると、 class SquareComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> button { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } </style> <button></button> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelector("button").innerHTML = this.props.value || ""; this.root.querySelector("button").onclick = this.props.onClick; } } customElements.define("square-component", SquareComponent); class BoardComponent extends HTMLElement { constructor() { super(); this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> .board-row:after { clear: both; content: ""; display: table; } </style> <div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> <div class="board-row"> <square-component></square-component> <square-component></square-component> <square-component></square-component> </div> </div> `; } setProps(props) { this.props = props; this.update(); } update() { this.root.querySelectorAll("square-component").forEach((elm, i) => { elm.setProps({ value: this.props.squares[i], onClick: () => this.props.onClick(i) }); }); } } customElements.define("board-component", BoardComponent); class GameComponent extends HTMLElement { constructor() { super(); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; this.root = this.attachShadow({ mode: "closed" }); this.root.innerHTML = ` <style> :host > div { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } </style> <div> <div> <board-component></board-component> </div> <div class="game-info"> <div></div> <ol></ol> </div> </div> `; this.update(); } setState(state) { this.state = { ...this.state, ...state }; this.update(); } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } update() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; const fragment = document.createRange().createContextualFragment(` <li> <button>${desc}</button> </li> `); fragment.querySelector("button").onclick = () => { this.jumpTo(move); } return fragment; }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } this.root.querySelector("board-component").setProps({ squares: current.squares, onClick: (i) => this.handleClick(i) }); this.root.querySelector(".game-info > div").innerHTML = status; this.root.querySelector(".game-info > ol").replaceChildren(...moves); } } customElements.define("game-component", GameComponent); function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; } となる。 実際に動いているものはここで確認可能だ。 https://htsnul.github.io/vanilla_js_react_tutorial/ まとめ Reactの見どころはDOMの差分適用が大きいが、 それ以外にも、コンポーネント化や、単方向データフローなどもある。 今回は、Vanilla JSで手動での差分更新は行っているが、 コンポーネント化や、単方向データフローは実現できている。 本チュートリアルの見どころのタイムトラベルなども実現できている。 手動での差分更新が許容できるならば、このような作り方もあり得るかもしれない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js + Typescript で作成したWebアプリをVercelにデプロイした

概要 タイトルの通りですが、これまで作成してきたWebアプリをVercelにデプロイして完了したいと思います。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使ってgraphQLを通じてHasuraにクエリやミューテーションができるようにしました。 Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました リファクタリングぽい内容ですが、React customHooks を利用してロジックとUIを切り離すこともやりました。 この記事でデプロイまでを完了させて最終的なゴールを達成したいと思います。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 Hasuraのエンドポイントにアクセスコントロールをつける これまで使ってきたHasuraのエンドポイントは誰でもアクセス可能な状態です。なので、Vercelデプロイ前にアクセスコントロールをしたいと思います。 JWTトークンを用いた認証もありますが、今回はHasura admin secretのkeyを使った認証をやりたいと思います。 ENV vars > New Env Varまで行って Valueはなんでもいいです。そしてAddをクリックします。Launch Consoleをクリックします。 x-hasura-admin-secretがセットされていますね。これが有効になっている限り、APIにアクセスする際にkey: x-hasura-admin-secretとvalue(セットしたヤツ)を合わせないとアクセスできなくなります。 Next.jsでもHasuraから作成した、x-hasura-admin-secretのvalueを環境変数として持たせます。 env.local NEXT_PUBLIC_HASURA_KEY="xxxxxxxxxxxxxxxx" NEXT_PUBLIC_HASURA_URL="https://ryosuketter-hasura-test.hasura.app/v1/graphql" アクセスする際に、headerにkey: x-hasura-admin-secretの指定が必要なのでセットしましょう。 ついでに、NEXT_PUBLIC_HASURA_URLも環境変数化してます。 valueは.env.localから持ってきます。 lib/apolloClient.ts const createApolloClient = () => { return new ApolloClient({ ssrMode: typeof window === 'undefined', link: new HttpLink({ uri: process.env.NEXT_PUBLIC_HASURA_URL, headers: { 'x-hasura-admin-secret': process.env.NEXT_PUBLIC_HASURA_KEY, }, }), cache: new InMemoryCache(), }) } yarn buildしてyarn startすれば確認できると思います。 Vercelにデプロイ github上にレポジトリ作成 ローカル上のコードをコミットして、今度はgithub 画像の通りじゃなくてもOK。リモートレポジトリ作ってpushしましょう。 Vercel 今度はVercel側に行きます。Githubの認証が終わったらログインできます。 githubと紐づけたら作成したレポジトリと紐づけます。 Vercelはpush(branch: main)したら自動デプロイされるので、その前に動かしたいコマンドを設定します。 テスト書いてたら、テストがパスするかなどもできます(今回はテスト書いてませんが)。 テスト書いてたら、これを記載してもいいと思います。 yarn testしてパスしたらビルドが走ります。という意味です。 Vercel に 環境変数をセット ローカルで作成した2つの環境変数をセットしましょう。 では、Deployをクリックします。 こんな感じになると思います。デプロイできたらこうなります? 何かしらURLが生成されデプロイできたので、アクセスして今まで作った機能を検証してみてください。 Hasuraとの通信もできていると思います。 また、ユーザーを作成して、SGされたページを見ると、最初は新規作成ユーザーが反映されていなくて、2回目みたら反映されていることを確認できると思います。 さいごに 下記のような構成を学ぶ旅も本記事で完結です。 めっちゃ触りだけな内容なので、これから深く学んでいこうと思います。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む