- 投稿日:2020-12-08T21:02:03+09:00
puppeteer で E2E テストにチャレンジして挫折した話
本記事は「Develop fun!」を体現する Works Human Intelligence #2の 9 日目です。
弊社ではアドベントカレンダーを 2 枚実施しておりますので、1 枚目もぜひご覧ください!本日は、私のチームで 1 年ちょっと前に導入しかけて挫折した「puppeteer で E2E テスト」についての備忘をまとめようと思います。
ことのはじまり
さかのぼること 1 年と少し前、私が所属するチームでは新規サービスの開発を進めていました。
フロントエンドは React、バックエンドは AWS Lambda を使ったサーバレスな Single Page Application です。
開発の初期段階からどんどん自動テストを導入していこう!という思想のもと、
フロントエンド、バックエンドそれぞれの Unit Test に加え、E2E テストも機能の開発と同時に作成していくことになりました。
当時はいろいろと手探り状態だったことと、まだフロントエンドとサーバーサイドが繋がっていない状態だったため、
「とりあえずpuppeteerでフロントエンドだけ動かすE2Eテスト1 を作ってみて、PR 作成時に実行するようにしよう!」という流れになりました。先に結論
タイトルにもある通り、puppeteer での E2E テスト開発はわりと早い段階で挫折しました。
puppeteer とは?
プログラムから API で Chrome (Chromium) を操作することのできる Node.js で作られたライブラリです。
ブラウザの GUI 上から実行できることはほとんど、puppeteer からも実行可能です。
例えば、以下のようなことができます。
- ページのスクリーンショットや PDF の生成
- フォームの送信・UI テスト・キーボード入力などの自動化
- 最新の自動テスト環境の作成。 最新の JavaScript とブラウザ機能を使用して、最新バージョンの Chrome で直接テストを実行する
- サイトの timeline trace をキャプチャして、パフォーマンスの問題の診断に使う
- Chrome 拡張機能のテスト
上記は puppeteer の README から引用しました。
README でもテストについての言及があるので、素直に E2E テストに利用してみることにしました。開発環境
1 年と少し前の話なので、ちょっと古い&記憶が曖昧なのですが、だいたいこんな感じだったと思います。
React プロジェクトの環境構築はcreate-react-app --typescript
で行いました。
- React 16.6.3
- Jest 23.6.0
- puppeteer 1.12.2
- jest-puppeteer 3.9.0( jest と一緒に puppeteer を利用する場合、jest-puppeteer を使うのが jest 公式でも推奨されています)
- TypeScript 3.2.2
- Node.js 10.15.0(たぶん)
また、該当サービスのリポジトリは monorepo(複数の package をまとめて 1 つのリポジトリで管理する)となっており、同じリポジトリ内にフロントエンドの package とサーバーの package が共存しています。
テスト実行の流れ
Jest + puppeteer での E2E テスト実行の流れは以下のようなイメージです。
Jest でのテスト実行としてはオーソドックスなパターンだと思います。
- Jest の GlobalSetup(すべてのテストの実行前に、テスト環境をセットアップするスクリプト)を用いて、puppeteer のローンチとテスト対象の画面の起動(今回の場合は自プロダクトのフロントエンドの Build と起動)を行う
- テストコードを実行する
- Jest の GlobalTearDown(GlobalSetup の逆で、全てのテストの実行後の後処理を行うスクリプト)を用いて、puppeteer とテスト対象画面を終了する
なお、この時点ではフロントエンドとサーバーサイドが繋がっていない状態なので、GlobalSetup ではテスト対象の画面(フロントエンド)の起動のみを行なっています。
大変だったところ
その1: セットアップが(ちょっと)大変
create-react-app でセットアップしたプロジェクトでは、デフォルトで用意されている
react-scripts test
を使うことであらかじめ用意された Jest の設定でテストを実行することができます。
しかし、今回は上述のように jest-puppeteer を使ってテスト実行のためのセットアップを行うため、Jest の設定は自前で用意する必要があります。まずは画面の起動のため、jest-puppeteer-config.js を用意し、puppeteer の起動設定を記載します。
jest-puppeteer-config.jsmodule.exports = { Server: { command: `react-scripts build && serve -s build`, // クライアントの起動コマンド port: 5000, // ポート指定 launchTimeout: 30000, // タイムアウト時間(ms) debug: true, // debug モードで起動 }, launch: { headless: true, // ヘッドレス起動 }, }次に、create-react-app が提供する Jest の設定ではなく自前で Jest の設定を行う必要があるため、jest.config.js も用意します。
E2E テスト用とわかるように、ファイル名は jest.e2e.config.js としておきます。jest.e2e.config.jsmodule.exports = { preset: 'jest-puppeteer', transform: { '^.+\\.jsx?$': 'babel-jest', '^.+\\.tsx?$': 'ts-jest', }, moduleFileExtensions: [ 'ts', 'tsx', 'js', 'jsx', 'json', 'node' ], transformIgnorePatterns: ['/node_modules/'], }最後に、テスト実行は
react-scripts test
ではなくjest
コマンドを直接呼ぶよう、package.json の scripts にコマンドを追加します。
その際に、さきほど用意した jest.e2e.config.js を参照するように設定します。package.json{ "name": "プロダクト名", "scripts": { "test:it": "jest -c './jest.e2e.config.js' --testMatch '**/__tests__/**/?(*.)+(spec).ts?(x)'", ... }, ... }ここまでの設定で、「フロントエンドを起動して画面を操作するテスト」は実行できるようになりました。
この時点ではまだフロントエンドとサーバーサイドが繋がっていない状態だったのでこれで十分でしたが、
今後開発が進めばフロントエンドから画面を操作してサーバに API を飛ばすところまで含めたテストが必要となってきます。
となると、GlobalSetup でフロントエンドの起動だけでなく、サーバーの起動まで必要になってきます。jest-puppeteer の機能で GlobalSetup を上書きできる機能があるのでサーバー起動もできないことはないですが、ちょっとめんどくさいです。
その2: 待ち時間を考慮してテストを書かないといけない
puppeteer は純粋なテスト用のツールというわけではないので、画面を操作するときに「待ち時間」を考慮してくれません。
例えば、
- 画面上に「購入」ボタンがある
- 「購入」ボタンをクリックするとサーバーと通信し、購入処理が走る
- サーバーから処理完了の応答が返ってきたら、購入完了画面に遷移する
というような画面のテストをしたい場合、
- 「購入」ボタンをクリックする
- 購入処理が完了するまで待つ
- 処理完了後、購入完了ページに遷移することを確認する
というテストを作成する必要がありますが、
2 番目の「待つ」という部分は自前で書く必要があります。待ち時間を短くしすぎると待機時間が足りずにテストに失敗する可能性が高くなってしまいます。
しかし、待ち時間を長くしすぎるとテスト全体の実行時間が長くなってしまいます。また、SPA ではその特性上、とある操作をしたときだけ html 上に現れる要素を多用します。
例えば、
- 画面右上に「ヘルプ」ボタンがある
- 「ヘルプ」ボタンをクリックすると、ヘルプが記載されたポップアップが表示される
- ポップアップの外側をクリックすると、ポップアップが閉じる
というような画面の場合、
SPA では「ヘルプ」ボタンがクリックされたという状態のときだけ html 上にポップアップ部分が現れ、
ポップアップが閉じると html からもポップアップ部分が消える、
というような実装をすることが多いです。この挙動のテストをしたい場合、
- 「ヘルプ」ボタンをクリックする
- ポップアップが表示されることを確認する
- ポップアップの外側をクリックする
- ポップアップが閉じることを確認する
というテストを作成する必要がありますが、
2 と 4 の確認のタイミングに html の描写が追いつかず、
- ポップアップが表示される前にポップアップを確認しようとして失敗する
- ポップアップが消える前にポップアップが閉じたことを確認しようとして失敗する
という状況が多発します。
サーバーの処理の待ち時間、html の描写の待ち時間、どちらの場合も理想は
- 処理が完了するまでいい感じに待つ
- 不具合を疑うほど長い待ち時間だったら失敗と判定する
という形なのですが、puppeteer ではなかなか実現が難しいです。
最終的にどうなったか
セットアップはなんとか完了して E2E テストを実行できる状態にできましたが、
待ち時間の問題からテスト実行が全く安定しませんでした。
PR の作成時に E2E テストを実行するにしても、2 回に 1 回は何かを待ちきれずに E2E テストがエラーを吐き、
失敗を通知する赤いメッセージが Slack に送られてくる状態。
このままではとても開発が進まないということで、puppeteer での E2E テストは頓挫しました。代わりに E2E テストフレームワークとして Cypress を採用することとなりました。
E2Eテストフレームワークに求めるもの
puppeteer での E2E テスト挫折経験から得た、E2E テストフレームワークに求めることは以下の通りです。
- 自前で「待機」を書かなくていい
- 画面の切り替えや通信を含む処理など、フレームワーク側でいい感じに待ってほしいです。
- あまりに長い時間「待機」していたらエラーにしてほしい
- どのくらいまでの待ち時間なら許容するかを設定できるといいです。
- フレームワーク 1 つで完結している
- テスト用のフレームワークとブラウザごとのドライバが必要、とかだと面倒です。
- puppeteer は puppeteer だけでいいのでここはよかったです。
- テスト実行の様子や実行結果を確認できる
- 特にテストに失敗したとき、なんで失敗したのかをテスト実行後に確認できるといいです。
- テスト終了時の画面のスクショや、できれば動画があるといいです。
おわりに
以上、puppeteer で E2E テストを実現しようとして挫折した話でした。
最後まで読んでいただきありがとうございました!
今思うと厳密には End to End とは言えない... ↩
- 投稿日:2020-12-08T18:34:32+09:00
Reactアプリ作成時にエラー"'--jsx' フラグが指定されていないと、JSX を使用できません。"が発生する場合の対処
現在
npx create-react-app hello-world --template typescript上記コマンドで作成したプロジェクトをVSCode等で開くと、
各種tsxファイルにおいて
'--jsx' フラグが指定されていないと、JSX を使用できません。
上記エラーが発生する場合があります。
これは使用しているTypescriptのバージョンが古いことに起因するものです。解決方法
ワークスペースのTypescriptを使用しましょう。
以下VSCodeの例①VSCode右下のTypescriptのバージョンが表示されている箇所をクリック
④VSCode再起動
以上でエラーは出なくなっているはずです。
お困りの方の手助けになれば幸いです。
- 投稿日:2020-12-08T15:14:03+09:00
大学でコンピュータ科学を学びながら、個人でアプリやフロントを書き続けた結果得られたもの
0. はじめに
こんにちは、とうようと言います。
普段は東大大学院のコンピュータ科学専攻の学生をしていたり、Life is Tech !というところで中高生にiOS開発などを教えたり、個人や受託の形でiOS/AndroidアプリやReact.jsを使ったWebサービスを作ったりしています。今回のアドベントカレンダーは「プログラミング技術の変化で得られた知見・苦労話」がテーマということで、少し変化球ではありますが、大学で情報科学・コンピュータ科学の古典的な知識を学びながら、個人の活動で最新の技術を追い続けるようなスタイルをとっていた大学生活で何が得られたのか、どんなことが良かったのかということをポエムにしていこうと思います。
今の中高生というと、一部の物好きがプログラミングをしていた僕の中高生時代からだいぶ進んできて、企業でインターンしたり、自分で起業できるぐらいのコーディング力をすでに現段階で身につけている子がゴロゴロいるような世代になってきています。
そんな子達の進路相談なども時々受けたりするのですが、その際よく観点として上がるのが「大学に行くべきなのかどうか?」という点です。もちろんこの質問自体にはいろんな観点がありますし、そもそもコンピュータ科学を大学で学ぶべきか、後から興味がわいた時に自分で学べばいいのでは?みたいな話もあったりするので、一概には言えないのですが、そういう中高生たちやこれからコンピュータ科学を独学しようかと思っているエンジニアのみなさんの参考になるのではないかと思い今回の記事を書くことに決めました。
前置きが長くなってしまいましたが、最後に大前提として僕のスペックを箇条書きにしておくので「このくらいのレベル感の人が得られた知見」だと割り切って、少しでも参考にしてもらえればと思います。
- 大学の専門科目のテストは総合して際立っていいわけでもめちゃくちゃ悪いわけでもなかった。落単は基本的にしていなかったが、定着度もそこそこぐらいだった。
- iOSアプリ開発は個人でいくつかリリースしたのと、受託の手伝いやサマーインターンをこなしたぐらい。ただし教える立場上ある程度最新の情報は追っていたり、学生の中ではそれなりに技術力があるという程度のレベル感。
- Webフロントについては研究プロトタイプや、先日リリースされたオンライン劇場zaでNext.jsを活用していたというぐらい。そこまでちゃんと最新技術を追えているわけではないが基本不自由なく書けるぐらいのレベル感。
- プログラミング自体は中高生からやっていて、そのころは競技プログラミングを主にやっていた。
1. コンピュータ科学って結局何を学ぶの?
まずはじめに、どんなことを学ぶのかの全体像がわかってないと今後の話もあやふやになると思うのでまずはそこから行きたいと思います。
というわけで今回は自分の成績表から科目名をざっくり引っ張り出してきてみました。一部選択科目などもあるので一概にこれがコンピュータ科学の科目です!とは言い切れないのですが、東京大学理学部情報科学科・東京大学大学院情報理工学系研究科コンピュータ科学専攻で学べるシラバスの一例としてみてもらえればと思います。
それではドン!
科目名 簡易説明 情報数学 情報分野で扱うような数学、符号理論とか群環体あたりの話 形式言語理論 オートマトンとか正規言語とか 計算機システム 計算機の仕組み、アセンブリの知識など ハードウェア構成法 ASICや論理回路設計の話 アルゴリズムとデータ構造 基本的なアルゴリズムを学ぶ 情報科学基礎実験 C,Scheme,アセンブリで色々実装してプログラミングの基礎を叩き込む コンピュータグラフィクス論 CGの仕組み学んだり、実際に実装したり オペレーティングシステム OSの仕組みを実習もしながら学ぶ 離散数学 グラフ理論、線形計画法とか 情報論理 一階述語論理とか、不完全性定理とか 言語処理系論 言語処理系を構築する理論とか構文解析について 情報科学演習I 離散数学と情報論理の問題を解いていく 知能システム論 人工知能の基本的なこと 計算機構成論 計算機アーキテクチャについて、特定のCPUについて調べて発表したり 言語モデル論 プログラミング言語のモデル、意味論、型推論のこととか 計算量理論 チューリングマシンやNP完全、近似アルゴリズムについて学ぶ 連続系アルゴリズム 浮動小数点や数値積分などを扱うアルゴリズム周りのあれこれ 情報科学演習II 計算量理論、連続系アルゴリズムの問題を解く コンピュータネットワーク インターネットの仕組みなどを学ぶ システムプログラミング実験 Linuxをいじったりシェルを作ったりする演習 関数・論理型プログラミング実験 関数型言語を学ぶ、PrologとかOCamlとか ハードウエア実験 VHDLを使って論理回路とかを実装 プロセッサ・コンパイラ実験 FPGAを使って3~4人チームでCPU,コンパイラ,シミュレーターなどを分担して作り、レイトレーシングプログラムを動かすことを目指す ユーザーインターフェイス ユーザーインターフェイスの歴史を学び、課題に応じてユーザーインターフェイスを実装 計算アルゴリズム論 数値積分や偏微分方程式の数値解法について 離散アルゴリズム論 離散アルゴリズム(確率を使ったものや量子系)について 計算科学シミュレーション HPCについて学び、HPC向けのアルゴリズムなどについて学ぶ 計算機言語論 コンパイラとかの理論の発展 自然計算 自然科学が関わるようなアルゴリズムなどについて、セルオートマトンとか 量子計算科学 量子コンピュータとかで扱われるもろもろについて 知識処理論 機械学習の授業 統計的機械学習 機械学習の中でも統計で扱うようなものについて メディア情報学 ユーザーインターフェイスの論文を輪講した後、最後に題材を使って面白いものを作る 配列解析アルゴリズム特論 配列を扱うようなアルゴリズムに関して 言っても大学と大学院の4年半のカリキュラムの中で行われている授業の一覧なので多少端折ったとはいえ量は結構多いですね。
このように一般的なプログラミングや普段使っているパーソナルコンピュータの仕組みはもちろんのこと、アルゴリズム一つとっても通常のCPU、HPC、量子コンピュータそれぞれに向けたものを各科目で学んでいたりと、古典となる理論から最新の機械学習事情まで幅広く学ぶのが情報科学・コンピュータ科学を「アカデミックで」学ぶということになります。なおここではこのざっくり説明以上のことは本筋からも逸れてしまうので語りません。もっと詳しく知りたい方は学科のHPで色々みてみてください。
2. 大学の知識が役立った瞬間5選
ざっくり説明でどんなことをやっているかさらってもらったところで続いて大学で学んだことが役立った瞬間をいくつか例にあげていきたいと思います。
Case 1: 関数型の気持ちがわかった瞬間
まず1つ目は関数型の気持ちがわかった瞬間です。このこと自体は関数型が好きな人からしてみれば別に大学でやらなくてもというところだと思いますが、大学では関数型言語をただ学ぶだけでなくその仕組みや背景などもいろんな科目を通して学ぶことになります。
そのため中高時代は競技プログラミングメインで手続き型に慣れていて、なかなか関数型を使いこなすレベルまで行けていなかった自分でも関数型の気持ちがわかってきて、SwiftやTypeScriptを書くときも関数型的な機能を自然に使いこなせるようになりました。
Case 2: 自分のする解説・理解に厚みが出た瞬間
次は自分のする解説・理解に厚みが出た瞬間です。例えばプログラミング言語のほとんどが配列を0-indexedで扱うようにしているということ自体は有名な話ですが、これを初学者に説明する場合みなさんならどのように解説するでしょうか?
普通に考えると「そういうふうになってるから注意してね」というようなワンポイント解説的なものに留まってしまうことが多いのではないでしょうか?
ですが僕はこれに関してちょうどOSやらシェルやらを扱っている授業を通して次のような気付きを得ました。「Cの配列とポインタって同じように扱えるよな、array[3]とarray + 3が同じ意味になると考えると0-indexedになるのは確かに自然な実装だよな」
さらに
「ポインタってことはこれはメモリ上でのアドレスを示しているよな、配列の先頭のアドレスを配列の名前で表せるとして、そこから連続して値が入ってるわけだから配列のsubscriptにどのくらいアドレスをずらすと目的の値がみれるのか渡せば目的の値を取り出すのに一番必要な論理演算少なく取り出せるよな、だから1-indexedにはしないのか」
みたいな。(※ 個人での気づきなので本当にこう考えて設計されたという証拠はあまりないです)
もちろんこれをそのまんま解説しても基礎がわかっていない人には何の話じゃという感じになってしまいますが、ある程度理解してきたときに「実は...」的な感じで話せばプログラミングっておもしろい!って思ってもらえる一つのきっかけ程度にはなるんじゃないでしょうか。
このように多角的にコンピュータのことを学んでいるからこその気付きが出てくるのがアカデミックに学ぶ利点の大きなところかなと思っています。Case 3: 知見のない分野に「こうすればいいのでは力」を発揮できた瞬間
3つ目は知見のない分野に「こうすればいいのでは力」を発揮できた瞬間です。
IT技術って本当に多岐に渡りますよね。そんな中一つの分野について極めてくるとその分野の新しい技術や仕組みに出会ったときに「こうすればうまくいくんじゃないか?」みたいな予感が働くことってあるんじゃないでしょうか。こういう力っていうのは通常、自分が深く関わっている分野に何かしら関連がないと働かないものだと思いますが、アカデミックなコンピュータ科学では広範囲に手を広げているので、かなりの範囲でその力を働かせることができるようになります。
これはもちろんその分野をかじっていて、そこまで覚えてなくても記憶の片隅に何か残ってるから、というのもありますが、もっと大きな要因としては各分野のつながりをわかっているからというものがあるのではないかなと思っています。だからこそ自分の引き出しの中からこれに当てはめれば似たようなやり方でできそう、がかなりの広範囲でできるということです。あくまで個人的な感覚でしかないですが、おそらくこのおかげでかなりフルスタックにカバーできる技術力を身につけられたのではないかなと思います。
Case 4. 曲芸のようなデバッグができた瞬間
4つ目は曲芸のようなデバッグができた瞬間です。
アプリ開発やサービス開発をしていると往々にして「謎のバグ」に立ち向かうハメになることはありますよね。
そんなときその分野での一般的なデバッグ方法が役に立たない瞬間というのもあるのではないでしょうか?そこで「何となくこんな感じの原因な気がする」とあたりをつけて全くの別角度から攻めてデバッグ成功する、こういう曲芸というか飛び技というかができるのがコンピュータ科学を知っている強みだと思います。
Case 5. 日常の開発にアルゴリズムを活用できた瞬間
最後は日常の開発にアルゴリズムを活用できた瞬間です。これはどちらかというと大学の知識、というより競技プログラミング力なのかもしれませんが、何か日常の開発で「こういうロジックを組めばうまくいきそう」とアルゴリズムを組んでいけるのは大きな強みかなと思っています。
以上他にも自分で気づいてないうちに恩恵を受けている部分はもっとあると思いますがその中でも印象深いものを例にあげてみました。
このような恩恵が活かせるのが「コンピュータ科学」です。3. 技術の進化に関係なく使える力
さて、本編最後としてアドベントカレンダーのテーマにもかけて技術の進化に関係なく使える力というテーマで語っていきたいと思います。
ここまで紹介してきた大学で学ぶ学問としてのコンピュータ科学、幅広いとは言いましたがそのほとんどが現代型計算機ができたノイマンの時代、さらにはその前から着実に積み上げられたもので最近の技術の発展のスピードから見たら古典的な知識が多いことは事実だと思います。
一見これらの知識は不要なものかもしれません。事実、現代のエンジニアの中にはここらへんの知識を一切知らないけど活躍している方は相当数いると思います。ですがそんな知識でも役に立つ瞬間が数多くあるのは前の章で見てもらった通りだと思います。進化の早いこの世界、3年もすれば古くて使い物にならないなんてざらなこの業界でなぜそんなことがありうるのでしょうか?
そこに僕は技術の進化に関係なく使える力というものが関わってくると思います。正直に言うと、僕自身最初に定着度はそこそこと述べた通り各授業の詳細はほとんど覚えていません。もともと授業を受けている間も苦手だった離散数学の問題なんか持ってこられても1ミリも解けずに終わってしまうと思います。またHPCやOSをいじることなんてこの先ソフトウェアエンジニアとして生きていく中で少なくとも仕事ではほとんどないと思います。これらの知識はある意味直接的には使わない知識です。
ですがどんなに知識を忘れていても考え方のエッセンスみたいなものは身についていると思います。それこそが技術の進化に左右されない力なのではないでしょうか。
もう少し踏み込んでこれを説明してみましょう。物事の定着にはいかに自分のすでに持っている知識と結びつけられるかが大事です。僕自身はそれをここまで語ってきたコンピュータ科学の分野、さらにリアルタイムに最新の技術を追うことでその知識も絡めて行うことができました。
つまり、コンピュータの過去から現在を一気通貫でリンクする、そんな経験を大学時代にできたわけです。これが何よりの僕の経験で得られた財産であると思っており、これこそが考え方のエッセンスそのものなのではないかと考えています。4. 番外編: 大学に入ったからこそ得られた機会
おまけとして大学に入ったからこそ得られた機会についてみていこうと思います。
これは結論から言ってしまいますが、一番は「人」だと思います。ありきたりの答えですが、この結論は揺るがないかもなと思っています。
大学には本当に多様な人がいます。特に総合大学だとなおのことでしょう。
法学部でバリバリのアプリエンジニアになったサークル同期がいたと思えば、全くプログラミングをしていなかったクラスの同級生がCTOになってたり、同じ学科の中でも理論が好きな人からゲーム制作が得意な人までさまざまな人がいるのが大学の多様性です。(もちろんエンジニア関連以外にもいろんな人がいますよね、同じ学科から吉本興業のマネージャーに就職した人なんかもいます)
もちろんインターンを渡り歩いたりいろんなところに顔を出すことで同じ経験を大学に行かずとも得られる人もいるとは思います。ですがアカデミック志向な人ってなかなか、大学じゃないと出会えないんじゃないでしょうか。いや、別にそんな例を無理に見つけ出さなくても、自分から頑張ってようやくその機会を得られるのか、いるだけでその機会を得られるのかは多分雲泥の差だと思います。
そんな利点があるのが、大学です。これを見て利点と思わなかったり、もっと大事なことがあるひとは別の道を選ぶのもありなのかなと思います。
あくまでこの記事は「参考」でしかないので。
5. まとめ
以上大学でガチガチの情報科学・コンピュータ科学を学びながら、趣味や仕事ではフロントやアプリの最新技術を追い続けた僕なりの知見をつらつらと語ってみました。
ちなみにこの記事は「だから○○がいいよ」と宣伝するような意図の記事ではありません。あくまで「冒頭で述べたようなスペックの学生がこういう経験をするとこんなことを得られるよ」という一例であるので、ぜひそこを十分に踏まえた上で進路を決めたり、コンピュータ科学を学ぶ上での参考にしてもらえればなと思います。
また手前味噌ですが、今年のアドベントカレンダーでこの記事とは別に今度は「教える側」について語った記事も用意しています(あわせると卒業文集になります。嘘です。修論がんばります。)
研修をする際や、自分で学ぶ際、ぜひこちらも参考にしてみてください。それではみなさん、メリークリスマス!笑
illustrated by unDraw
- 投稿日:2020-12-08T15:10:28+09:00
【React】Presentational Component と Container Component
Component
- ReactはUIを
Component
という部品単位で扱うPresentational Component
とContainer Component
に分けることで、見た目とロジックを切り分けることができるPresentational Component
- 表示のみに専念する
- 自分のコンポーネント以外のことについて依存しない
- 親からpropsとしてデータやコールバックを受け取る
components/App.jsimport React from 'react'; const App = ({ text: string, onButtonClick: () => void }) => ( <div> <p>{text}</p> <button onClick={onButtonClick}>Click</button> </div> ); export default App;Container Component
- ロジックのみに専念する
- Presentational Componentに具体的なデータやコールバックを渡す
containers/App.jsimport App from "../components/App"; import { connect } from "react-redux"; const mapStateToProps = state => { return { text: state.text }; }; const mapDispatchToProps = dispatch => { return { onButtonClick: () => { dispatch({ type: "CLICK" }); } }; }; export default connect(mapStateToProps, mapDispatchToProps)(App);Component の分離によるメリット
- アプリケーション部分とUI部分を分離できる
- 表示するデータやボタン押下時の処理を外部から指定することができる
-> 再利用性が上がる
- 投稿日:2020-12-08T12:21:15+09:00
ts migrateを使ってReactのアプリをJSからTSに書き換えよう!!
皆さんこんにちは。
最近、毎日の様にTypeScriptという単語が耳に入ってきて、「TypeScriptって伸びてるみたいだし、そんなにいいのか」と思い、いろいろ調べていたのですが、どうやらなかなかに良さそうな言語だったので、基礎的な部分を少し勉強してみました。
そこで、「試しにReactのアプリもjsxからtsxに置き換えてみるか」と思い、調べていると
ts migrate
という、Airbnbが開発したパッケージがあり、これを利用するのが良さそうだとわかったので、試しに使ってみたところめっちゃ便利だったので、今回はts migrate
についての使い方の記事を書こうと思いました。ts migrateってなに?
ts migrateの公式リポジトリの説明によれば、
ts-migrate はコードの TypeScript への移行を支援するツールです。JavaScript、または部分的な TypeScript プロジェクトを取り込み、コンパイルした TypeScript プロジェクトを出力します。ts-migrate は TypeScript の移行プロセスを高速化することを目的としています。結果として得られるコードはビルドを通過しますが、型の安全性を向上させるためのフォローアップが必要です。たくさんの // @ts-expect-error や、時間をかけて修正する必要があるものがあるでしょう。一般的には、ゼロから始めるよりもずっといいでしょう。
とのことです。
まとめると、
- TypeScriptへの移行ツール
- TypeScriptへの移行を(手作業と比べて)高速化することが目的
- しかし、変換されたコードは型の安全性が低いので修正が必要
こんな感じです。
少し補足すると、
コマンド一発で選択したディレクトリ配下のjsファイルを全てtsファイルに変換し、中身の関数などにも型をつけてくれるもの
です。
ただ、型は全てany
なので後から変えていく必要があるよ、ということです。実際に使ってみる
さて、実際に使ってみましょう。
以下の
使い方
のところで出てきますが、先にgitをクリーンな状態にしておきましょう。ローカルでの変更がremoteにcommitされているかmasterが最新の状態になっているか確認しましょう。確認ができたら、作業用のブランチを切ります。
$ git checkout -b ts-migrateインストール
インストールは簡単です。
npmの場合は
$ npm i ts-migrateyarnの場合は
$ yarn add ts-migrateこれで、インストール完了です!
使い方
以下のコマンドで、ファイルをTS化できます。
$ npx ts-migrate-full <folder>このの部分は変換したいフォルダを選択します。今回は、Reactのsrcフォルダ配下のファイルをtsx化したいので、
$ npx ts-migrate-full srcとして実行します。
実行すると、以下の様なとても親切なメッセージが表示されます。This script will migrate a frontend folder to a compiling (or almost compiling) TS project. It is recommended that you take the following steps before continuing... 1. Make sure you have a clean git slate. Run `git status` to make sure you have no local changes that may get lost. Check in or stash your changes, then re-run this script. 2. Check out a new branch for the migration. For example, `git checkout -b ts-migrate` if you're migrating several folders or `git checkout -b ts-migrate-src` if you're just migrating src. 3. Make sure you're on the latest, clean master. `git fetch origin master && git reset --hard origin/master` 4. Make sure you have the latest npm modules installed. `npm install` or `yarn install` If you need help or have feedback, please file an issue on Github! Continue? (y/N)
訳すと、
- git をクリーンな状態にしておきましょう。git status` を実行して、ローカルでの変更が失われていないことを確認します。チェックインするか変更を保存してから、このスクリプトを再実行します。
- 移行のための新しいブランチをチェックアウトします。例えば、複数のフォルダを移行する場合は
git checkout -b ts-migrate
とします。srcを移行するだけならgit checkout -b ts-migrate-src
。- 最新のクリーンなマスターを使用していることを確認してください。
git fetch origin master && git reset --hard origin/master
です。- 最新の npm モジュールがインストールされていることを確認してください。
npm install
またはyarn install
を実行する。全部OKならyを入力。
変換が開始されます。All done! The recommended next steps are... 1. Sanity check your changes locally by inspecting the commits and loading the affected pages. 2. Push your changes with `git push`. 3. Open a PR!
いろいろログが出た後に、上記の様に
All done!
とでていれば成功です。ちゃんと変換ができています!!(感動)
jsxファイルは以下の様にtsxファイルになりました。
関数コンポーネントも
Post.tsxconst Post = ({content, createdAt, title, uid}: any) => { //以下省略この様に型がつけられています?
いろいろなerrorを消していこう
簡単に変換はできましたがいろいろerrorが出るので、それらを直していきます。
よくあるのは、
@ts-expect-error
です。この
@ts-expect-error
がコメントで表示されているので、各ファイルのコメントを消していきましょう。コメントを消すと特になにも起こらない箇所もありますが、errorが起きる箇所もあります。僕の場合は、コメントを消してerrorが起きたところは大抵
型がないパターン
だったのでとりあえずany
をつけました。元々のファイルの数が多いと、結構errorを潰していくのが面倒です...w
全てのerrorを消したら完了です!!
お疲れ様でした‼️
- 投稿日:2020-12-08T12:21:15+09:00
ts migrateを使ってReactのアプリをJavaScriptからTypeScriptに書き換えよう!!
皆さんこんにちは。
最近、毎日の様にTypeScriptという単語が耳に入ってきて、「TypeScriptって伸びてるみたいだし、そんなにいいのか」と思い、いろいろ調べていたのですが、どうやらなかなかに良さそうな言語だったので、基礎的な部分を少し勉強してみました。
そこで、「試しにReactのアプリもjsxからtsxに置き換えてみるか」と思い、調べていると
ts migrate
という、Airbnbが開発したパッケージがあり、これを利用するのが良さそうだとわかったので、試しに使ってみたところめっちゃ便利だったので、今回はts migrate
についての使い方の記事を書こうと思いました。ts migrateってなに?
ts migrateの公式リポジトリの説明によれば、
ts-migrate はコードの TypeScript への移行を支援するツールです。JavaScript、または部分的な TypeScript プロジェクトを取り込み、コンパイルした TypeScript プロジェクトを出力します。ts-migrate は TypeScript の移行プロセスを高速化することを目的としています。結果として得られるコードはビルドを通過しますが、型の安全性を向上させるためのフォローアップが必要です。たくさんの // @ts-expect-error や、時間をかけて修正する必要があるものがあるでしょう。一般的には、ゼロから始めるよりもずっといいでしょう。
とのことです。
まとめると、
- TypeScriptへの移行ツール
- TypeScriptへの移行を(手作業と比べて)高速化することが目的
- しかし、変換されたコードは型の安全性が低いので修正が必要
こんな感じです。
少し補足すると、
コマンド一発で選択したディレクトリ配下のjsファイルを全てtsファイルに変換し、中身の関数などにも型をつけてくれるもの
です。
ただ、型は全てany
なので後から変えていく必要があるよ、ということです。実際に使ってみる
さて、実際に使ってみましょう。
以下の
使い方
のところで出てきますが、先にgitをクリーンな状態にしておきましょう。ローカルでの変更がremoteにcommitされているかmasterが最新の状態になっているか確認しましょう。確認ができたら、作業用のブランチを切ります。
$ git checkout -b ts-migrateインストール
インストールは簡単です。
npmの場合は
$ npm i ts-migrateyarnの場合は
$ yarn add ts-migrateこれで、インストール完了です!
使い方
以下のコマンドで、ファイルをTS化できます。
$ npx ts-migrate-full <folder>このの部分は変換したいフォルダを選択します。今回は、Reactのsrcフォルダ配下のファイルをtsx化したいので、
$ npx ts-migrate-full srcとして実行します。
実行すると、以下の様なとても親切なメッセージが表示されます。This script will migrate a frontend folder to a compiling (or almost compiling) TS project. It is recommended that you take the following steps before continuing... 1. Make sure you have a clean git slate. Run `git status` to make sure you have no local changes that may get lost. Check in or stash your changes, then re-run this script. 2. Check out a new branch for the migration. For example, `git checkout -b ts-migrate` if you're migrating several folders or `git checkout -b ts-migrate-src` if you're just migrating src. 3. Make sure you're on the latest, clean master. `git fetch origin master && git reset --hard origin/master` 4. Make sure you have the latest npm modules installed. `npm install` or `yarn install` If you need help or have feedback, please file an issue on Github! Continue? (y/N)
訳すと、
- git をクリーンな状態にしておきましょう。git status` を実行して、ローカルでの変更が失われていないことを確認します。チェックインするか変更を保存してから、このスクリプトを再実行します。
- 移行のための新しいブランチをチェックアウトします。例えば、複数のフォルダを移行する場合は
git checkout -b ts-migrate
とします。srcを移行するだけならgit checkout -b ts-migrate-src
。- 最新のクリーンなマスターを使用していることを確認してください。
git fetch origin master && git reset --hard origin/master
です。- 最新の npm モジュールがインストールされていることを確認してください。
npm install
またはyarn install
を実行する。全部OKならyを入力。
変換が開始されます。All done! The recommended next steps are... 1. Sanity check your changes locally by inspecting the commits and loading the affected pages. 2. Push your changes with `git push`. 3. Open a PR!
いろいろログが出た後に、上記の様に
All done!
とでていれば成功です。ちゃんと変換ができています!!(感動)
jsxファイルは以下の様にtsxファイルになりました。
関数コンポーネントも
Post.tsxconst Post = ({content, createdAt, title, uid}: any) => { //以下省略この様に型がつけられています?
typescriptをインストールしていない場合はインストール
npm install typescript
tsconfigの設定
tsconfigは以下の様に設定しました。
tsconfig.json{ "compilerOptions": { "jsx": "react", "target": "es5", "module": "esnext", "strict": true, "noImplicitAny": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": [ "src" ] }いろいろなerrorを消していこう
簡単に変換はできましたがいろいろerrorが出るので、それらを直していきます。
よくあるのは、
@ts-expect-error
です。この
@ts-expect-error
がコメントで表示されているので、各ファイルのコメントを消していきましょう。コメントを消すと特になにも起こらない箇所もありますが、errorが起きる箇所もあります。僕の場合は、コメントを消してerrorが起きたところは大抵
型がないパターン
だったのでとりあえずany
をつけました。元々のファイルの数が多いと、結構errorを潰していくのが面倒です...w
全てのerrorを消したら完了です!!
お疲れ様でした‼️参考
ts-migrate(公式)
ts-migrate: A Tool for Migrating to TypeScript at Scale
JSからTSへの移行ツール、ts-migrateを試してみた
Introduce TypeScript to react(js) project with ts-migrate?
- 投稿日:2020-12-08T11:30:02+09:00
rel="noopener noreferrer" の必要性
rel="noopener noreferrer"とは
noopenerの指定
noopener を指定することで、リンク先からwindow.openerを使ってリンク元が参照できなくなります。さらに、リンク先とリンク元が別のものとして扱われるためパフォーマンスに対しての対策にもなっているのです。
参考サイト
https://html.spec.whatwg.org/multipage/semantics.html#link-type-noopenernoreferrerの指定
noreffererを指定することで、リンク先にリンク元のリンク情報が送られないようになるため、 noopenerと同じくリンク先からの参照ができなくなります。
ブラウザによっては、noopenerがサポートされていないため、noopenerと合わせてnoreffererを指定するのが好ましいと言えます。
noreferrerを当てるだけでESlintのエラーに対応できそうですが、noreferrer noopener
としっかり書かないとだめみたいです。参考サイト
https://html.spec.whatwg.org/multipage/semantics.html#link-type-noreferrer抜粋It indicates that no referrer information is to be leaked when following the link.意訳リンクをたどるときにリファラー情報漏らさないよ抜粋<a href="..." rel="noreferrer" target="_blank"> has the same behavior as <a href="..." rel="noreferrer noopener" target="_blank">.意訳noreferrerとnoreferrer noopenerは同じ以上
- 投稿日:2020-12-08T10:50:18+09:00
stateに入れたJSON配列の一部を変更する
stateにJSON配列を入れて
それぞれのJSONをタブで分けて表示している時に
各タブで値を編集した時に
stateへの値の戻し方がよくわからなかったので備忘録としてconst [configs, setConfigs] = useState( [ { kousinsitaiatai:"aaaaa" }, { kousinsitaiatai:"bbbbb" } ] );{configs.map((config, index) => ( <TabPanel value={value} index={index}> <TextField label="更新したい値" defaultValue={config.kousinsitaiatai} onChange={e => handleValueChange(index, e)}/> {/*↑↑↑indexも一緒に渡す*/} </TabPanel> ))}const handleValueChange= (index: number, e: any) => { let configs_copy = configs.slice(); configs_copy[index].kousinsitaiatai = e.target.value; setConfigs(configs_copy); };
- 投稿日:2020-12-08T10:40:40+09:00
【React】Immutable.js から Immer へ切替
なぜ
ずっと
immutable.js
使ってますが、OOP
アプローチに関して、最新何かあるかを探したことろ、immer
が見つかった。なんと、React open source award 2020 のBreakthrough of the year
を受賞されたようです。どっちが良い
現時点、どっちもメジャーな
OOP
アプローチです。immutable.js
は Facebook が管理するライブラリで、結構年数ありました。過去2年のダウンロード数から見ると、最近はimmer
が波に乗ってます。サイズやパフォーマンス的にもimmer
が良さそうです。個人感想のメリット
immutable.js
はredux
用のライブラリで、immer
はReact.setState
にも使えます。
type
の定義が必要なくて、楽になって、コード量は大きく減ってませんが、可読性が向上したと思います。変更点 - Settings
redux-immutable
とconnected-react-router
のimmutable
ライブラリが必要なくなります。変更点 - Reducer
reducer
から見ると、import
のみ変わって、ほとんど修正必要ありません。reducer-immutable.jsimport { handleActions, Action } from 'redux-actions'; import { AppState } from '@domains'; import { ActionTypes } from '@constants'; import { App01Payload, App02Payload } from '@actions/app'; const app = handleActions<AppState, any>( { [ActionTypes.APP_PLUS_REQUEST]: (state: AppState) => state, [ActionTypes.APP_PLUS_SUCCESS]: (state: AppState, { payload: { num } }: Action<App01Payload>) => state.plus(num), [ActionTypes.APP_PLUS_FAILURE]: (state: AppState) => state, [ActionTypes.APP_MINUS_REQUEST]: (state: AppState) => state, [ActionTypes.APP_MINUS_SUCCESS]: (state: AppState, { payload: { num } }: Action<App02Payload>) => state.minus(num), [ActionTypes.APP_MINUS_FAILURE]: (state: AppState) => state, }, new AppState() ); export default app;reducer-immerimport { handleActions, Action } from 'redux-actions'; import { AppState } from '@domains'; import { ActionTypes } from '@constants'; import { App01Payload, App02Payload } from '@actions/app'; const app = handleActions<AppState, any>( { [ActionTypes.APP_PLUS_REQUEST]: (state: AppState) => state, [ActionTypes.APP_PLUS_SUCCESS]: (state: AppState, { payload: { num } }: Action<App01Payload>) => state.plus(num), [ActionTypes.APP_PLUS_FAILURE]: (state: AppState) => state, [ActionTypes.APP_MINUS_REQUEST]: (state: AppState) => state, [ActionTypes.APP_MINUS_SUCCESS]: (state: AppState, { payload: { num } }: Action<App02Payload>) => state.minus(num), [ActionTypes.APP_MINUS_FAILURE]: (state: AppState) => state, }, new AppState() ); export default app;変更点 - ロジック
従来の
typescript type
定義が省略できるので、少し楽になります。書き方もimmutable.js
独自の方法じゃなく、ES6
が書けます。immutable.jsimport { Record } from 'immutable'; export interface IApp extends AppProps, Record<AppProps> { get<K extends keyof AppProps>(key: K): AppProps[K]; } export interface AppUIProps {} export interface AppProps extends AppUIProps { // count count: number; } /** * App */ export default class AppState extends Record<AppProps>({ count: 0, }) { plus(num: number) { return this.set('count', this.count + num); } // inner update minus(num: number) { return this.set('count', this.count - num); } }immerimport { immerable, produce } from 'immer'; export default class AppState { [immerable] = true; count: number = 0; plus = (num: number) => produce(this, (draft) => { draft.count += num; }); minus = (num: number) => produce(this, (draft) => { draft.count -= num; }); }おまけ
immer
の公式ドキュメントは詳しく色んな情報書いています。
immer
取り込み済みのプロジェクトは GitHub 公開中です。
- 投稿日:2020-12-08T10:26:59+09:00
React + Unstated Next: リデューサを使ったうえでコンテナに変換する
Unstated Nextは、複数コンポーネントにより組み立てられたツリーの中で、状態を共有して管理するライブラリです。基本的な使い方は「React + Unstated Next: 複数コンポーネントのツリーの中で状態を共有して管理する」で、簡単なカウンターのサンプルをつくりながらご説明しました(図001)。
図001■Unstated Nextを使ったカウンター
本項では、カウンターのサンプルにリデューサ(reducer)を加えます。そのうえで、コンテナに変換して、状態の操作と保持のロジックを分離してみます。上記にリンクしたカウンターのサンプルに手を加えるかたちで進めましょう。
リデューサを使う
リデューサを使うと、コンテナから値の保持が切り分けられます。用いるフックは
useReducer
です。引数にはつぎのように、リデューサ関数と初期状態(オブジェクト)を渡します。戻り値は配列で、要素は現行の状態(state
)とアクション配信関数(dispatch
)のふたつです。const [state, dispatch] = useReducer(リデューサ関数, 初期状態);
useReducer
は、コンテナモジュールsrc/useCounter.js
からつぎのように呼び出します。dispatch
は、アクションと呼ばれるオブジェクトをリデューサに送る関数です。アクションには何が起こったかを示すtype
プロパティが含まれ、必要に応じてその他のデータも加えられます(今回用いたアクションはtype
プロパティしかもちません)。プロバイダから子コンポーネントに渡したい状態は、useReducer
が返した配列要素のstate
から参照してフックの戻り値に加えてください。src/useCounter.js// import { useCallback, useState } from "react"; import { useCallback, useReducer } from 'react'; const useCounter = (initialState = 0) => { // const [count, setCount] = useState(initialState); const [state, dispatch] = useReducer(reducer, { count: initialState }); // const decrement = useCallback(() => setCount((prevCount) => prevCount - 1), []); const decrement = useCallback(() => dispatch({type: 'decrement'}), []); // const increment = useCallback(() => setCount((prevCount) => prevCount + 1), []); const increment = useCallback(() => dispatch({type: 'increment'}), []); // return { count, decrement, increment }; return { count: state.count, decrement, increment }; };新たなリデューサモジュール
src/reducer.js
の記述は以下のコード001のとおりです。イベントと違って、アクションごとのハンドラはもちません。そのため、アクションのtype
プロパティに応じて処理を分けるswitch
文で組み立てます。アクションの配信と同じく、リデューサも状態(state
)を直にはいじりません。改めた状態のプロパティをオブジェクトに収めて返すだけです。これで、
useReducer
フックにより状態の保持が切り分けられました。書き直したカスタムフックのモジュールsrc/useCounter.js
も、併せてコード001に掲げます。コード001■useReducerフックで状態の保持を切り分ける
src/reducer.jsconst reducer = (state, action) => { switch (action.type) { case 'decrement': return {count: state.count - 1}; case 'increment': return {count: state.count + 1}; default: return state; } }; export default reducer;src/useCounter.jsimport { useCallback, useReducer } from 'react'; import { createContainer } from 'unstated-next'; import reducer from './reducer'; const useCounter = (initialState = 0) => { const [state, dispatch] = useReducer(reducer, { count: initialState }); const decrement = useCallback(() => dispatch({type: 'decrement'}), []); const increment = useCallback(() => dispatch({type: 'increment'}), []); return { count: state.count, decrement, increment }; }; export const CounterContainer = createContainer(useCounter);リデューサをコンテナにする
さらに、リデューサ(
react:src/reducer.js
)もコンテナにしてみましょう。コンテナにすることで、ロジックがよりはっきり切り分けられます。コンテナにするには、まずカスタムフックに書き替えなければなりません。フックの関数(
useCounterReducer
)を新たに加え、useReducer
はその中から呼び出します。戻り値は、子コンポーネントに共有する参照が収められたオブジェクトです。カスタムフックをcreateContainer
に渡して、コンテナをつくってください。src/reducer.jsimport { useReducer } from 'react'; import { createContainer } from 'unstated-next'; const initialState = { count: 0 }; const useCounterReducer = () => { const [state, dispatch] = useReducer(reducer, initialState); return { dispatch, count: state.count }; } // export default reducer; export default createContainer(useCounterReducer);状態を操作するコンテナ(
src/useCounter.js
)は、もはやuseReducer
は用いず、リデューサコンテナ(reducer
)に対して呼び出したuseContainer
から参照を得ます。注目していただきたいのは、useReducer
と異なり状態(state
)が丸ごと直に触れないことです。何を参照してよいかは、リデューサコンテナが決められます。src/useCounter.js// import { useCallback, useReducer } from 'react'; import { useCallback } from 'react'; const useCounter = (initialState = 0) => { // const [state, dispatch] = useReducer(reducer, { count: initialState }); const { count, dispatch } = reducer.useContainer(reducer); // return { count: state.count, decrement, increment }; return { count, decrement, increment }; };リデューサコンテナ(
reducer
)もプロバイダでコンポーネントツリーを包みます。状態を操作するコンテナ(CounterContainer
)はリデューサを参照しますので、リデューサの子にしなければなりません。src/App.jsimport reducer from './reducer'; function App() { return ( <reducer.Provider> <CounterContainer.Provider> </CounterContainer.Provider> </reducer.Provider> ); }状態操作のコンテナ(
src/useCounter.js
)に加えて、リデューサもコンテナ(src/reducer.js
)にしました。書き替えた3つのモジュールの記述は、つぎのコード002にまとめたとおりです。他のモジュールの記述や動きについては、以下のサンプル002をご覧ください。コード002■リデューサをコンテナに変換した
src/reducer.jsimport { useReducer } from 'react'; import { createContainer } from 'unstated-next'; const reducer = (state, action) => { switch (action.type) { case 'decrement': return {count: state.count - 1}; case 'increment': return {count: state.count + 1}; default: return state; } }; const initialState = { count: 0 }; const useCounterReducer = () => { const [state, dispatch] = useReducer(reducer, initialState); return { dispatch, count: state.count }; } export default createContainer(useCounterReducer);src/useCounter.jsimport { useCallback } from 'react'; import { createContainer } from 'unstated-next'; import reducer from './reducer'; const useCounter = (initialState = 0) => { const { count, dispatch } = reducer.useContainer(reducer); const decrement = useCallback(() => dispatch({type: 'decrement'}), []); const increment = useCallback(() => dispatch({type: 'increment'}), []); return { count, decrement, increment }; }; export const CounterContainer = createContainer(useCounter);src/App.jsimport React from 'react'; import reducer from './reducer'; import { CounterContainer } from './useCounter'; import CounterDisplay from './CounterDisplay'; import './App.css'; function App() { return ( <reducer.Provider> <CounterContainer.Provider> <div className="App"> <CounterDisplay /> </div> </CounterContainer.Provider> </reducer.Provider> ); } export default App;サンプル001■リデューサをコンテナに変換したカウンター
参考:「Create React App フックによる状態管理 04: カスタムフックにUnstated Nextを組み合わせる」
- 投稿日:2020-12-08T02:58:51+09:00
Next.jsでRouter.pushを使うときに「No Router instance found」のエラーが発生
概要
Next.jsのルーティングの記事にある通り、Next.jsでは
Router.push
を使うことでURL履歴のpushとreplaceが可能です。例えば、コンポーネントに渡ってくるプロパティなどをチェックして不備があれば、別のパスに遷移するような使い方が考えられます。
今回はこのRouter.push
を使うときに、「No Router instance found」のエラーが発生するケースがあったので、それについて書いてみたいと思います。どんなときにエラーが起きるのか
こちらのissueのやりとりにある通り、SSRでRouterのメソッドを使うことはできません。ので、コンポーネントがマウントされる前に呼び出しを行うとエラーが発生します。
対応
上記issueのコメントにある通り、
componentDidMount
の中で呼び出しを行うか、windowsが読み込まれたタイミングで呼び出しを行えば、エラーが発生するのを防げます。windowsが読み込まれたタイミングで、呼び出しを行う実装サンプルは以下の通りです。Sample.jsimport Router from "next/router"; export default function Sample(prop) { if (typeof window !== "undefined") { if (!prop.userId) { Router.push("/"); } } return ( <div> propチェックOK </div> ); }
- 投稿日:2020-12-08T02:46:47+09:00
React Hooks(追加のHook)
追加のフック
React組み込みのフックのうち、追加のフックを試していきます。
- useReducer
- useRef
- useCallback
- useMemo
useReducer
状態管理のためのフックです。
useStateと似た機能です。useStateとuseReducer
useStateはuseReducerで実装されています。
(=useStateでできることは全てuseReducerでもできるということになります)useStateとuseReducerの比較
useState useReducer 扱えるstateのtype 数値、文字列、真偽値 オブジェクト、配列 関連するstateの取り扱い 不可 複数可能 ローカル/グローバル ローカル グローバル(useContext()を併用)
キーワード 説明 state 現在の値 reducer stateを更新するための関数(stateとactionを受け取って、stateを返す) dispatch reducerを実行するための呼び出し関数 action
何をするのかを示すオブジェクトです。
typeプロパティと値のプロパティで構成します。jsx{ type:increment, payload:0 }jsxconst recducer関数名 = (state, action)=> { switch (action){ case アクションのtype1: return {処理A} case アクションのtype2: return {処理B} case アクションのtype3: return {処理C} default: return state } } const [state, dispatch] = useReducer(reducer関数名,'初期値')useReducer()を使ってカウンターを作ってみる
stateが単数の場合
src/Counter.js//useReducerをimport import React, {useReducer} from 'react' //counterの初期値を0に設定 const initialState = 0 //reducer関数を作成(countStateとactionを渡して、処理後のcountStateを返すように実装する) const reducerFunc = (countState, action)=> { //reducer関数にincrement、increment、reset処理を書く //どの処理を渡すかはactionを渡すことによって判断する switch (action){ case 'increment': return countState + 1 case 'decrement': return countState - 1 case 'reset': return initialState default: return countState } } //コンポーネントCounterを作成 const Counter = () => { //作成したreducerFunc関数とcountStateをuseReducerに渡す //useReducerで変数名、変更に使う手法、reducer関数、初期値を紐付けする //useStateと同じく、[値、変更する手法]=useReducer(reducerで使う関数,初期値)初期値が無いと初期表示がブランクになってしまう //使う時は、変更する手法('reducerの処理の分岐の判定に使うキーワード') const [count, dispatch] = useReducer(reducerFunc, initialState) return ( <> {/* カウント数の表示 */} <h2>{count}</h2> <div> {/* それぞれのactionを実行するボタン */} <button onClick={()=>dispatch('increment')}>+1</button> <button onClick={()=>dispatch('decrement')}>-1</button> <button onClick={()=>dispatch('reset')}>初期化</button> </div> </> ) } export default Countersrc/App.jsx// ReactからcreateContextとuseStateをimport import React from 'react' //Conterコンポーネントをインポート import Counter from './Counter' function App() { return ( <div className='App'> <Counter /> </div> ) } export default Appstateが複数の場合
1つのstateが扱うデータを複数にして、増減の値も変えてみます。
src/Counter.2jsx//useReducerをimport import React, {useReducer} from 'react' //counterの初期値を0に設定 //2つのcountStateを作るので、それぞれのinitialStateを設定 const initialState ={ firstCounter: 0, secondCounter: 0 } //reducer関数を作成 //countStateとactionを渡して、新しいcountStateを返す const reduceFunction = (countState, action)=> { //switch文のactionをaction.typeに変更(typeとvalueを渡したいのでオブジェクトにして渡す) //複数のcounterStateを持っている場合は、更新前のcounterStateを展開し、オブジェクトのマージを行う switch (action.type){ case 'increment1': return {...countState, firstCounter: countState.firstCounter + action.value} case 'decrement1': return {...countState, firstCounter: countState.firstCounter - action.value} case 'increment2': return {...countState, secondCounter: countState.secondCounter + action.value} case 'decrement2': return {...countState, secondCounter: countState.secondCounter - action.value} case 'reset1': return {...countState, firstCounter: initialState.firstCounter} case 'reset2': return {...countState, secondCounter: initialState.secondCounter} default: return countState } } const Counter2 = () => { //作成したreducerFunc関数とcountStateをuseReducerに渡す //useReducerで変数名、変更に使う手法、reducer関数、初期値を紐付けする //useStateと同じく、[値、変更する手法]=useReducer(reducerで使う関数,初期値)初期値が無いと初期表示がブランクになってしまう //使う時は、変更する手法('reducerの処理の分岐の判定に使うキーワード') const [count, dispatch] = useReducer(reduceFunction, initialState) return ( <> {/* カウント数の表示 */} <h2>カウント1:{count.firstCounter}</h2> {/* それぞれのactionを実行するボタン */} {/*dispatchで渡すactionをオブジェクトに変更して、typeとvalueを設定*/} <button onClick={()=>dispatch({type: 'increment1', value: 1})}>+1</button> <button onClick={()=>dispatch({type: 'decrement1', value: 1})}>-1</button> <button onClick={()=>dispatch({type: 'reset1'})}>初期化</button> {/* カウント数の表示 */} <h2>カウント2:{count.secondCounter}</h2> {/* それぞれのactionを実行するボタン */} {/*dispatchで渡すactionをオブジェクトに変更して、typeとvalueを設定*/} <button onClick={()=>dispatch({type: 'increment2', value: 2})}>+2</button> <button onClick={()=>dispatch({type: 'decrement2', value: 2})}>-2</button> <button onClick={()=>dispatch({type: 'reset2'})}>初期化</button> </> ) } export default Counter2App.jsx// ReactからcreateContextとuseStateをimport import React from 'react' //Conter2コンポーネントをインポート import Counter from './Counter2' function App() { return ( <div className='App'> <Counter2 /> </div> ) } export default AppuseRef
要素への参照を行うことが出来ます。
useStateのようにコンポーネント内での値を保持することも出来ます。値を保持する
変数にuseRefを使って .currentプロパティ内に値を保持させられます。
値は変数名.currentで参照できます。jsxconst refObject = useRef(initialValue) //例 const number = useRef(100); // {current: 100} console.log(number.current); // 100DOMを参照する
jsconst inputElement = useRef(null) //例 <input ref={inputElement} type="text" /> console.log(inputElement); // current: nulljsimport React, {useRef} from 'react' const RefSample=()=>{ //変数inputElementという参照ポイントを作成 const inputElement = useRef(null); console.log(inputElement);//{current: null} console.log(inputElement.current);//null //関数handleClick const handleClick = () => { console.log(inputElement); //{current: input} console.log(inputElement.current); // <input type="text"> //inputElement.current(=<input type="text">)にフォーカスする inputElement.current.focus(); }; return ( <> {/* inputを参照ポイント「inputElement」と紐付ける */} <input ref={inputElement} type="text" /> {/* ボタンを押すとhandleClickを実行 */} <button onClick={handleClick}>入力エリアをフォーカスする</button> </> ); } export default RefSample;useCallback
パフォーマンス低下抑制のためのフックです。
メモ化したコールバック関数を返します。メモ化
初回の処理を記録しておきます。
依存配列の要素のいずれかが変化した場合のみ、メモ化した値を再計算することで、不要なレンダリングを抑制します。jsxconst callback = useCallback(関数,[依存配列])Reactコンポーネントの再レンダリング条件
条件を1つでも満たした場合には再レンダリングされます。
条件 内容 propsの更新 親の再レンダリングによる無意味なpropsの更新の場合、useCallbackで抑制 stateの更新 レンダリングは必要なので、メモを破棄してレンダリングする 親コンポーネントが再レンダリング時 不要な場合React.memoで抑制 入力された文字をカウントする
コンポーネントがレンダリングされるとconsole.logで確認できるようにします。
メモ化していない場合
src\App.jsimport React,{useState} from 'react' import InputElementA from './InputElementA'; import InputElementB from './InputElementB'; import CountA from './CountA'; import CountB from './CountB'; //コンポーネントApp const App = () => { //親要素が持つstate変数inputTextAを定義 const [inputTextA, setInputTextA] = useState(""); //親要素が持つstate変数inputTextBを定義 const [inputTextB, setInputTextB] = useState(""); //state関数inputTextAを更新する関数inputTextFunctionAを定義 const inputTextFunctionA=(text)=> { setInputTextA(text); } //state関数inputTextBを更新する関数inputTextFunctionBを定義 const inputTextFunctionB=(text)=> { setInputTextB(text); } return( <> {/* inputAに入力された文字列を表示 */} <CountA>{inputTextA}</CountA> {/* 子要素に親要素の関数を渡す */} <InputElementA inputTextFunctionA={e => inputTextFunctionA(e)}/> {/* inputに入力された文字列を表示 */} <CountB>{inputTextB}</CountB> {/* 子要素に親要素の関数を渡す */} <InputElementB inputTextFunctionB={e => inputTextFunctionB(e)}/> </> ) } export default Appsrc\InputElementA.jsimport React from 'react' const InputElementA = (props) => { console.log('InputElementAがレンダリングされた') return ( <> <input onChange={(e) => props.inputTextFunctionA(e.target.value)}/> </> ) } export default InputElementAsrc\InputElementB.jsimport React from 'react' const InputElementB = (props) => { console.log('InputElementBがレンダリングされた') return ( <> <input onChange={(e) => props.inputTextFunctionB(e.target.value)}/> </> ) } export default InputElementBsrc\CountA.jsimport React from 'react' const CountA = ({children}) => { console.log('CountAがレンダリングされた') return ( <div> {children.length} </div> ) } export default CountAsrc\CountB.jsimport React from 'react' const CountB = ({children}) => { console.log('CountBがレンダリングされた') return ( <div> {children.length} </div> ) } export default CountBメモ化してレンダリング削減する場合
コンポーネントにはuseReact.memo、関数にはuseCallbackを使って不要なレンダリングを抑制します。
console.logを確認すると、不要なレンダリングが抑制されています。src\App.jsimport React,{useState,useCallback} from 'react' import InputElementA from './InputElementA'; import InputElementB from './InputElementB'; import CountA from './CountA'; import CountB from './CountB'; //コンポーネントApp const App = () => { //親要素が持つstate変数inputTextAを定義 const [inputTextA, setInputTextA] = useState(""); //親要素が持つstate変数inputTextBを定義 const [inputTextB, setInputTextB] = useState(""); //state関数inputTextAを更新する関数inputTextFunctionAを定義 const inputTextFunctionA=useCallback((text)=> { setInputTextA(text); },[setInputTextA]); //state関数inputTextBを更新する関数inputTextFunctionBを定義 const inputTextFunctionB=useCallback((text)=> { setInputTextB(text); },[setInputTextB]); return( <> {/* inputAに入力された文字列を表示 */} <CountA>{inputTextA}</CountA> {/* 子要素に親要素の関数を渡す */} <InputElementA inputTextFunctionA={useCallback((e) =>{ inputTextFunctionA(e)},[])}/> {/* inputに入力された文字列を表示 */} <CountB>{inputTextB}</CountB> {/* 子要素に親要素の関数を渡す */} <InputElementB inputTextFunctionB={useCallback((e) =>{ inputTextFunctionB(e)},[])}/> </> ) } export default Appsrc\InputElementA.jsimport React from 'react' const InputElementA = React.memo((props) => { console.log('InputElementAがレンダリングされた') return ( <> <input onChange={(e) => props.inputTextFunctionA(e.target.value)}/> </> ) }); export default InputElementAsrc\InputElementB.jsimport React from 'react' const InputElementB = React.memo((props) => { console.log('InputElementBがレンダリングされた') return ( <> <input onChange={(e) => props.inputTextFunctionB(e.target.value)}/> </> ) }); export default InputElementBsrc\CountA.jsimport React from 'react' const CountA = React.memo(({children}) => { console.log('CountAがレンダリングされた') return ( <div> {children.length} </div> ) }); export default CountAsrc\CountB.jsimport React from 'react' const CountB = React.memo(({children}) => { console.log('CountBがレンダリングされた') return ( <div> {children.length} </div> ) }); export default CountBuseMemo
値を保存するためのhookです。
何回やっても結果が同じ場合の値などをメモ化し、そこから値を再取得します。
不要な再計算をしないので、パフォーマンス低下を抑制できます。依存配列に入れた変数に変化があった場合に即時に更新されます。
依存配列に空の配列を渡すと初回のみ実行されます。jsxconst memoSample = useMemo(() => { //処理 }, [依存配列])useCallbackとuseMemoの違い
useCallbacl/useMemo 特徴 用途 useCallback 関数自体をメモ化 子要素に関数の参照を渡す場合に使う useMemo 関数の結果をメモ化 子要素に関数の参照を渡す場合以外に使う ボタンを押すとカウントアップし、カウントアップした数の2乗を表示する
useMemoを使わない場合
Ajsimport React,{useState} from 'react' //コンポーネントApp const App = () => { const[countA,setCountA]=useState(0); const[countB,setCountB]=useState(0); const increaseA=()=>{ setCountA(prevCount=>prevCount+1); } const increaseB=()=>{ setCountB(prevCount=>prevCount+1); } const doubleA=()=>{ console.log("重い処理A") return countA*countA; } const doubleB=()=>{ console.log("重い処理B"); return countB*countB; } return( <> <p>カウントA:{countA}</p> <p>カウントAの二乗:{doubleA()}</p> <button onClick={increaseA}>+1</button> <p>カウントB:{countB}</p> <p>カウントBの二乗:{doubleB()}</p> <button onClick={increaseB}>+1</button> </> ) } export default AppuseMemoを使う場合
useMemoを使った部分console.logが実行されなくなっていることがわかります。
jsimport React,{useState,useMemo} from 'react' //コンポーネントApp const App = () => { const[countA,setCountA]=useState(0); const[countB,setCountB]=useState(0); const increaseA=()=>{ setCountA(prevCount=>prevCount+1); } const increaseB=()=>{ setCountB(prevCount=>prevCount+1); } const doubleA=useMemo(()=>{ console.log("重い処理A"); return countA*countA; },[countA]) const doubleB=useMemo(()=>{ console.log("重い処理B"); return countB*countB; },[countB]) return( <> <p>カウントA:{countA}</p> <p>カウントAの二乗:{doubleA}</p> <button onClick={increaseA}>+1</button> <p>カウントB:{countB}</p> <p>カウントBの二乗:{doubleB}</p> <button onClick={increaseB}>+1</button> </> ) } export default Appその他の追加のフック
useImperativeHandle
ref使用時に親コンポーネントに渡されるインスタンス値をカスタマイズするのに使います。
useLayoutEffect
useEffectと似た効果のフックです。
フック 説明 useEffect 「レンダリングが終了した後」に実行 useLayoutEffect 「DOMに追加後ブラウザ表示する直前」に実行 useDebugValue
デバッグに使うフックです。
デバッグ情報をReact Dev Toolsに表示できます。
- 投稿日:2020-12-08T02:45:50+09:00
React Hooks(基本のHook)
基本のフック
React組み込みのフックのうち、基本のフックを試していきます。
- useState
- useEffect
- useContext
useState
トグル、値の管理に使えます。
useStateを使わずに変数を定義した場合、コンポーネントの再描画時にリセットされてしまいます。
useState によって React の state の機能を関数コンポーネントに追加します。const [count, setCount] = useState(intialState)トグル
src/Toggle.js//関数コンポーネント内で state を使えるようにするため、useState をインポート import React, { useState } from 'react' //関数コンポーネントCounterを定義 const Toggle = () => { // open という名前の state 変数を宣言、初期値 true をセット const [condition, setCondition] = useState(true) // 関数toggleを作成(state変数のconditionがtrueだとtrue、falseだとfalseを返す) const toggle = () => setCondition(!condition) return ( <> {/* ボタンを押すと関数toggleを実行する*/} <button onClick={toggle}>{condition ? 'アクティブ' : '非アクティブ'}</button> </> ) } export default Togglesrc/App.jsimport React from 'react' import Toggle from './Toggle'; //コンポーネントApp const App =() => { return( <> <Counter/> </> ) } export default App値を管理する
数値をユーザー操作で増減させる
src/Cunter.js// 関数コンポーネント内で state を使えるようにするため、useState をインポート import React, { useState } from 'react' //関数コンポーネントCounterを定義 const Counter = () => { //定数initialState初期値として使う0を指定 const intialState = 0 //countという名前のstate変数を作成し、初期値にintialStateをセット(countを更新するときはsetCountを使う) const [count, setCount] = useState(intialState) return ( <> {/* 現在値を表示 */} <p>現在の数字は{count}です</p> {/* ボタンを押すとsetCount関数でcountを更新 setCount(現在値=>現在値+変更させる値)の形にする */} <button onClick={() => setCount(prevCount => prevCount + 1)}> + 1 </button> {/* ボタンを押すとsetCount関数でcountを更新 setCount(現在値=>現在値+変更させる値)の形にする */} <button onClick={() => setCount(prevCount => prevCount - 1)}> - 1 </button> {/* 定数initialStateを使って、countを初期値に戻す */} <button onClick={() => setCount(intialState)}>初期値に戻す</button> </> ) } export default Countersrc/App.jsimport React from 'react' import Counter from './Counter'; //コンポーネントApp const App =() => { return( <> <Counter/> </> ) } export default App複数の値を1つのState変数で管理する
大人と子供の数を1つのstate変数(オブジェクト)でカウントします。
1つのState変数で複数の値を管理する場合、一部の値更新しても全体が上書きされてしまいます。
(新しい値を与えていない項目は値が消えてしまいます。)そのため、スプレッド構文で一度state変数をすべて展開して、その上で更新したい値を上書きして、すべての項目を更新することが必要です。
src/Counter.js// 関数コンポーネント内で state を使えるようにするため、useState をインポート import React, { useState } from 'react' //関数コンポーネントCounterを定義 const Counter = () => { //定数initialState初期値として使う0を指定 const intialState = { adult:0, child:100 } //countという名前のstate変数を作成し、初期値にintialStateをセット(countを更新するときはsetCountを使う) const [count, setCount] = useState(intialState) return ( <> {/* 現在値の表示を表示 */} <p>大人は{count.adult}人です</p> <p>子供は{count.child}人です</p> {/* ボタンを押すとsetCount関数でcountを更新 setCount(現在値=>現在値+変更させる値)の形にする */} <button onClick={() => setCount({...count,adult:count.adult+1})}> 大人 + 1 </button> {/* ボタンを押すとsetCount関数でcountを更新 setCount(現在値=>現在値+変更させる値)の形にする */} <button onClick={() => setCount({...count,adult:count.adult-1})}> 大人 - 1 </button> {/* ボタンを押すとsetCount関数でcountを更新 setCount(現在値=>現在値+変更させる値)の形にする */} <button onClick={() => setCount({...count,child:count.child+1})}> 子供 + 1 </button> {/* ボタンを押すとsetCount関数でcountを更新 setCount(現在値=>現在値+変更させる値)の形にする */} <button onClick={() => setCount({...count,child:(count.child-1)})}> 子供 - 1 </button> {/* 定数initialStateを使って、countを初期値に戻す */} <button onClick={() => setCount(intialState)}>初期値に戻す</button> </> ) } export default Countersrc/App.jsimport React from 'react' import Counter from './Counter'; //コンポーネントApp const App =() => { return( <> <Counter/> </> ) } export default AppuseEffect
関数をレンダリング終了後に実行します。
副作用の処理(DOMの書き換え、変数代入、API通信など)に使います。jsxuseEffect(() => { console.log("レンダリングされた"); }) ### レンダリング終了ごとに決まった処理を行う ```:src/EffectSample.js import React, {useState, useEffect} from 'react' const EffectSample = () => { //state変数ocuntの作成(初期値0) const [count, setCount] = useState(0) //レンダリング後に実行する処理 useEffect(() => { console.log("レンダリングされた"); }) return ( <> <p>{`${count}回クリックされた`}</p> <button onClick={()=>{ setCount(prevCount => prevCount + 1) console.log("+1された"); }}> +1 </button> <button onClick={()=>{ setCount(0); console.log("resetされた"); }}> リセット </button> </> ) } export default EffectSamplesrc/App.jsimport React from 'react' import EffectSample from './EffectSample'; //コンポーネントApp const App =() => { return( <> <EffectSample/> </> ) } export default App特定の値が変わったときだけ適用させる
useEffectの第2引数に変数を依存配列として渡すことで、変数を監視して変更があったときに適用させます。
カウントを2種類に増やし、カウントBが変化したときのみuseEffectを作動させてみます。
useEffectの第2引数に、監視する変数をセットします。src/EffectSample.jsimport React, {useState, useEffect} from 'react' const EffectSample = () => { //state変数ocuntの作成(初期値0) const [countA, setCountA] = useState(0) const [countB, setCountB] = useState(0) //レンダリング後に実行する処理(countBが変動する結果のレンダリングに限定) useEffect(() => { console.log("カウントBが変化した"); },[countB]) return ( <> <p>{`カウントAが${countA}回クリックされた`}</p> <div> <button onClick={()=>{ setCountA(prevCount => prevCount + 1) console.log("カウントAが+1された"); }}> +1 </button> <button onClick={()=>{ setCountA(0); console.log("カウントAがresetされた"); }}> リセット </button> </div> <p>{`カウントBが${countB}回クリックされた`}</p> <div> <button onClick={()=>{ setCountB(prevCount => prevCount + 1) console.log("カウントBが+1された"); }}> +1 </button> <button onClick={()=>{ setCountB(0); console.log("カウントBがresetされた"); }}> リセット </button> </div> </> ) } export default EffectSamplesrc/App.jsimport React from 'react' import EffectSample from './EffectSample'; //コンポーネントApp const App =() => { return( <> <EffectSample/> </> ) } export default App初回レンダリング時のみ実行する
useEffectの第2引数に空の配列を渡すと、初回レンダー時だけに発動させられます。
クリーンアップ
クリーンアップを必要としない副作用の例
コードが実行されたあとすぐにそのことを忘れても構わないもの
- ネットワークリクエストの送信
- 手動での DOM 改変
- ログの記録
クリーンアップを必要とする副作用の例
メモリリークが発生しないようにクリーンアップが必要な処理
(何らかの外部のデータソースへの購読をセットアップするもの)
- イベントリスナー
- タイマー
- 非同期でデータを取得し画面に表示する
など
クリーンアップの記述方法
- cleanup() 関数
- アロー関数
jsxuseEffect(() => { elm.addEventListener('click', () => {}) // コンポーネントがアンマウントされるときに呼ばれる(アロー関数で書く場合) return () => { elm.addEventListener('click', () => {}) } // コンポーネントがアンマウントされるときに呼ばれる(cleanup()関数で書く場合) //return function cleanup() { // elm.addEventListener('click', () => {} //} }, [])useContext
Context
コンポーネントからグローバルにアクセスできるデータです。
コンポーネントツリーのデータの橋渡し
Context/Props 説明 Context 下の階層で Contextに収容されているデータにアクセスできる(すべての階層ごとに渡す必要性がなくなる) Props バケツリレー useContext
親からPropsで渡されなくても、Contextに入れたデータにアクセスできます。
説明 createContext コンテキスト作成 useContext コンテキストを呼び出す Contextを共有する一番外側で値を渡します。
jsximport React, {useContext} from 'react' //context作成してexport export const DataContext = createContext(); const Parent =() => { //contextで共有するデータを作成 const data = { key1: '値1', key2: '値2' }; return( {/* 共有するデータを渡す */} <DaraContext.Provider value={data}> <ChildComponent/> </DataContext.Provider> ) }Contextを使った値の引き渡し
親→曾孫に受渡しできます。
親要素
src/App.js// ReactからcreateContextとuseStateをimport import React, {createContext,useState} from 'react' //ContextA.jsのContextコンポーネントをインポート import Child from './Child' //createContextでContextを作成(DataContextとTextContext)してexport export const DataContext = createContext(); export const TextContext = createContext(); function App() { //dataを作成 const [data,setData] = useState({ name: '山田', age: '20' }); //textを作成 const [text,setText] = useState('こんにちは'); return ( <div className='App'> {/*DataContext.Providerを作成、valueには「data」をセット*/} <DataContext.Provider value={[data,setData]}> {/*TextContext.Providerを作成、valueには「text」をセット*/} <TextContext.Provider value={[text,setText]}> <Child/> </TextContext.Provider> </DataContext.Provider> </div> ) } export default App子要素
src/Child.jsimport React from 'react' import GrandChild from './GrandChild' const Child = () => { return( <GrandChild/> ) } export default Child孫要素
src/GrandChild.jsimport React from 'react' import GreatGrandChild from './GreatGrandChild' const GrandChild = () =>{ return( <GreatGrandChild/> ) } export default GrandChild曾孫要素
src/GreatGrandChild.js//useContextをインポート import React, {useContext} from 'react' //AppコンポーネントからDataContext, TextContextをimport import {DataContext, TextContext} from './App' const GreatGrandChild = () => { //変数dataにDataContextを代入 const [data,setData] = useContext(DataContext) //変数textにTextContextを代入 const [text,setText] = useContext(TextContext) return ( <p>{data.name}{data.age}歳「{text}」</p> ) } export default GreatGrandChildPropsを使った値の引き渡し
親→子→孫→曾孫の順にPropsを使って渡していきます(バケツリレー)。
親要素
src\App.js// ReactからcreateContextとuseStateをimport import React, {useState} from 'react' //ContextA.jsのContextコンポーネントをインポート import Child from './Child' function App() { //dataを作成 const [data,setData] = useState({ name: '山田', age: '20' }); //textを作成 const [text,setText] = useState('こんにちは'); return ( <div className='App'> <Child data={data} text={text}/> </div> ) } export default App子要素
src\Child.jsimport React from 'react' import GrandChild from './GrandChild' const Child = (props) => { return( <GrandChild data={props.data} text={props.text}/> ) } export default Child 孫要素 ```:src\GrandChild.js import React from 'react' import GreatGrandChild from './GreatGrandChild' const GrandChild = (props) => { return( <GreatGrandChild data={props.data} text={props.text}/> ) } export default GrandChild曾孫要素
src\GreatGrandChild.jsimport React from 'react' const GreatGrandChild = (props) => { return ( <p>{props.data.name}{props.data.age}歳「{props.text}」</p> ) } export default GreatGrandChild
- 投稿日:2020-12-08T02:29:35+09:00
Reactチュートリアル徹底解説!
はじめに
React公式チュートリアルを完了したので、アウトプット&初学者の方に向けた解説を目的として記事にしました。
実際の手順に関しては公式に細かく記載されているため、本記事では実装手順を省き、内部の動きについてかなり細かく噛み砕いて解説しました。
対象としては、
- 何となくは理解できたけど細かい所が不安・・・
- とりあえずチュートリアルを終わらせたけど何が起こっているのかよく分からん
- そもそもJavaScriptが分からねえ
上記のような方々を対象としています。
少し長めの記事ですが、本記事を読めば公式チュートリアルの内容をほぼ理解できるような内容になっているかと思いますので、最後までどうかお付き合い下さい。Squareコンポーネント
ここでは、三目並べのマス目部分を担当しているSquareコンポーネントについて解説します。
function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); }とは言っても、非常に単純な構成のため1点のみです。
1. クラスコンポーネントではなく関数コンポーネントを使用
チュートリアルのコンポーネント構成は、上から
Game > Borad > Square
となっており、stateは全てGameコンポーネントで管理されています。
そのため、単に要素をreturnするだけのSquareコンポーネントは、よりシンプルに記載できる関数コンポーネントで実装されています。Boardコンポーネント
Boardコンポーネントは、Squareのまとまりを管理しています。
render内に記載されている9つのrenderSquare
関数によって、Boradコンポーネント内にSquareコンポーネントを配置しています。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> ); } }先程と比べると少しだけコード量が増えてきましたね。
しかしここではrenderSquare
関数を押さえれば意味が理解できると思います。1.
Square value
{this.props.squares[i]}
のsquares
は、Gameコンポーネントで管理されている配列のstateです。
後述しますが、squares
は9つのマス目全てに関して、どこにどちらのマーク(☓か○か)が入っているのか情報を持っています。
引数として渡す数値をもとに、マス目の場所に応じたマークを呼び出し、Square毎のvalueに代入しています。2.
Square onClick
ここでは、Gameコンポーネント内の
handleClick
関数を呼び出すための処理が記載されています。
Gameコンポーネントのrender部分は以下です。<Board squares={current.squares} onClick={(i) => this.handleClick(i)} />Boardコンポーネント内の
onClick={() => this.props.onClick(i)}
によって、配置を表す引数を渡しつつhandleClick
関数を呼び出しています。
この記述をもとに、マス目毎にhandleClick
イベントを発動させることができるようになっています。3.
onClick
内の() =>
onClick={() => this.props.onClick(i)}
の中に記載されているアロー関数は何のためにあるのか?
答えはクラスメソッドをバインドするためです。JSX のコールバックにおける this の意味に注意しなければなりません。JavaScript では、クラスのメソッドはデフォルトではバインドされません。this.handleClick へのバインドを忘れて onClick に渡した場合、実際に関数が呼ばれた時に this は undefined となってしまいます。
公式ドキュメント:イベント処理つまり、今回のように末尾に
()
を付けずに関数を呼び出す際は、何かしらの形でバインドしなければなりません。
別の方法としては、Gameコンポーネントのコンストラクタ内に以下のように記載することで解決できます。this.handleClick = this.handleClick.bind(this);calculateWinnerコンポーネント
Gameコンポーネントに進む前に、まずは「勝利条件が成立しているか」を判定するcalculateWinnerコンポーネントについて、ほぼReactに関係ない部分ですが説明しておきます。
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; }1.
lines
定数蛇足ですが、三目並べの勝利条件は「縦・横・斜めのいずれかに同じマークが3つ並ぶこと」です。
後述するfor文で、lines
内の組み合わせ全てに、同じマークがあるかどうかを判定していきます。
ハードコーディングされていますが、今回は3マス×3マスかつ三目並べ専用なので問題はありません。2. for文
まず
const [a, b, c] = lines[i]
で、lines
内の組み合わせを分割代入しています。
例えばlines[2]
であれば、a, b, c
はそれぞれ6, 7, 8
が代入されます。
それらを9つのマス目全ての情報を持っているsquares
配列のインデックスとして使用し、if文内で総当たりの条件判定を行っている形です。Gameコンポーネント
いよいよ本題のGameコンポーネントです。大まかに機能を分けると、
1.state
2.handleClick
3.jumpTo
4.render
このあたりでしょうか。順を追って解説していきます。
コード全体
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> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') );1. state
constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], stepNumber: 0, xIsNext: true, }; }
history
ここではhistory
内部に配列のsquares
がセットされています。
まずArray(9).fill(null)
は「全てがnullである、サイズが9の配列」です。
この記述によりstateの初期値として、一切マークされていない、ゲームスタート時のボードがセットされます。
次に直接suqares
を保持するのではなく、わざわざhistory
内部にsquares
を保持している理由ですが、これは後で過去の着手を表示するためです。
後述しますが、ゲームが進行する度に1つずつvalue
が追加されたsquares
が増えていくイメージ。
stepNumber
render内にconst current = history[this.state.stepNumber];
と記載されている通り、ゲームの手番を管理するためのstateです。
history[]
にインデックスを渡すことで、現在のsquares
の状態をcurrent
に代入しています。
xIsNext
こちらは次の手番が○か☓かを管理するためのstateです。
同じくrender内にstatus = 'Next Player: ' + (this.state.xIsNext ? 'X' : 'O');
と記載されています。
見慣れない方も居るかもしれませんが、こちらは条件(三項)演算子と呼ばれる記法で、?
までが条件文、:
の左がtrue、右がfalse時の処理を表しています。
今回の例だとthis.state.xIsNext
がtrueならX、falseなら○と、より簡潔に記述できますね。2. handleClick
ここではイベント発火元Squareのインデックスを引数として、squareがクリックされた時の処理について解説します。
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, }); }
定数
history
現在のhistory
をsliceメソッドで「現在の手番 + 1」で作り直し、history
に代入しています。 ぱっと見は何のための処理か分からなくなりそうですが、これは後述のjumpTo
メソッドが実行された時に必要な処理です。jumpTo
メソッド内ではhistory
をsetStateしないので、メソッド実行後もhistory
の中身は変わりません。 しかし、このままではhistory
がおかしくなるため、handleClick
メソッド実行時に正しい中身に再設定している形です。 以下は前回画像の状況から過去の手番に戻った場面ですが、盤面とhistory
の数が一致していません。 空のマス目をクリックすることで`historyが作り直され、正しい履歴に戻ります。![]()
current
こちらは単に現在のhistory
のみを取得・代入しています。squares
cconst squares = current.squares.slice()
と繋げることで、盤面を更新する前に、現在の状態をsquares
に代入しています。処理
- if文
ここでは「ゲームの勝者が居る場合」 or 「マーク済のマスがクリックされた場合」に早期returnするためにif文が使用されています。
if (calculateWinner(squares) || squares[i])
となっているので、どちらかがtrueであればこの先の処理は行われません。- 条件演算子
先程のような構文が再び出てきましたね。ここではxIsNext
がtrueなら☓、falseなら○がクリックされたsquareのvalueとして代入されます。setState
history
少し前でhistory
を作り直した意味がやっとここで出てきます。
history.concat([{squares: squares,}])
とすることで、今までの履歴に今回作成されたsquares
を追加できます。
この一連の処理によって、今までの履歴を管理しつつ正しい順番で今回の処理結果を追加することができました。
stepNumber
どうせ使用時に+ 1するので単にhistory.length
を代入。xIsNext
!this.state.xIsNext
とする事で値を反転させています。
xIsNext
は真偽値なので、この処理の度にtrue / false切り替わる形です。3. jumpTo
jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); }
stepNumber
引数のstep
にはターゲットとなるhistory
のインデックスが入るため、stepNumber
に正しい値がセットされます。xIsNext
xIsNext: (step % 2) === 0
とすることでxIsNext
に正しい値がセットされます。4. render
いよいよ大詰めです。少し長いですが頑張りましょう!
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> ); }
定数
const history = this.state.history
現時点での全てのhistory
をセットconst current = history[this.state.stepNumber]
最新の盤面をセットconst winner = calculateWinner(current.squares)
現在の盤面での勝者をセットconst moves = history.map((step, move) => {}
まず、この処理内で使われているdesc
は「descending order」ではなく「description」です。(蛇足かもしれませんが、初見で勘違いしたので・・・)
ここではhistory
それぞれのインデックスを取得するためにmap処理を行っています。
なので「'step' が宣言されていますが、その値が読み取られることはありません。」と表示されていますが問題ありません。
const desc = move ? 'Go to move #' + move : 'Go to game start';
ここでまたもや条件演算子の登場です。
move
がtrue、つまり「0やnull以外の値」であればdesc
に'Go to move #' + move'
を代入、move
がfalseであれば'Go to game start'
を行います。
move
がfalse(今回だと0のみ)の場合はhistory[0]
=ゲームスタート時の盤面なので'Go to game start'
と表示させる寸法です。let status
空のstatus
を宣言しておき、if文内でwinner
の有無に応じて表示を出し分けています。
ここは非常にシンプルですね。return
ここではあまり説明することもありませんが1点だけ。
onClick={(i) => this.handleClick(i)}
でアロー関数の引数になっているi
ですが、これはBoardコンポーネント内のrenderSquare(i)
から来ています。
Square value
に渡しているインデックスをついでに利用した形です。あとは最後に
ReactDOM.render
して終了です!おわりに
ここまでお付き合い下さりありがとうございました。
本記事が少しでも皆様のお役に立てれば幸いです。
ほぼ初めての記事投稿だったのですが、作成にあたり自分の中でも理解が深まったと感じます。
散々言われている事ですが、やはり自身で言語化 → アウトプットする事は大切だなーとしみじみ・・・
これを機にアウトプットをしっかり癖付けていきます。、、、とはいえ今年は終わりが近いので来年から頑張ることにします。
それでは皆さま良いお年を。
- 投稿日:2020-12-08T01:19:23+09:00
Next.js + Deck.glを試してみる
MIERUNE Advent Calendar 2020 8日目の記事です。
7日目は@nokonoko_1203 さんの「QGISで3D巨大都市(の一部)を召喚する」でした!
今度「3Dでベル・ハーバー召喚してーな」ってなったら試してみようと思います!(いつなるんだろう....)
7日目は海外でベル・ハーバーだったので、今日は国内の東京でいろいろやろうと思います。TL;DR
Next.js + Deck.glをゆるくゆるく試しつつ、行政区域データ(市区町村の境界のデータ)を表示してみるっていう記事です。
Githubにコードがあります: https://github.com/Nekoya3/nextjs_deckgl_example
Netlifyで公開もしてますので試したい方はぜひ: https://gallant-goodall-a481e2.netlify.app/
ついでにVercelでも公開していますのでぜひ: https://nextjs-deckgl-example.vercel.app
(Vercelのほうが体感早い感じします...後ろで出てくるdata.jsonのせいで重たいですが...通常はAPIとかを使いましょう...)はじめに
Next.jsとは?
Next.jsのチュートリアルのはじめにはこうのっています
- 直感的なページベースのルーティングシステム(ダイナミックルートをサポート
- プリレンダリングでは、スタティック生成(SSG)とサーバーサイドレンダリング(SSR)の両方がページ単位でサポートされています。
- ページ読み込みを高速化するための自動コード分割
- 最適化されたプリフェッチによるクライアントサイドルーティング
- 組み込みの CSS と Sass のサポート、および任意の CSS-in-JS ライブラリのサポート
- 高速リフレッシュ対応の開発環境
- サーバーレス関数でAPIエンドポイントを構築するためのAPIルート
- 完全に拡張可能
https://nextjs.org/learn/basics/create-nextjs-app の Next.js: The React Framework から引用
DeepLで翻訳しました。要するに何よ???
Vercelによって開発されたJavaScriptフレームワークで、Reactと組み合わせてウェブアプリ開発を強化するフレームワークで。
Reactアプリのサーバーサイドレンダリング(SSR)を可能にするものでもある。
でも今はSSRよりもSSGを推してるっぽい(私もSSG好き)
(VueでいうNuxtみたいなものですね...)Deck.GLとは?
Deck.GLはUberがオープンソースで公開しているWebGLベースの地理情報可視化フレームワーク。
Reactでの使用が推奨されていますが、Vue.jsなどでも使用することができる。
Deck.GLには様々なLayerと呼ばれるものが用意されているので、簡単にオープンデータを可視化できる。手順
Next.js の初期設定
npx create-next-app
で作ってもいいんですが、余計なのがついてくるし、Typescriptで書きたいので手動でInitializeします。YarnのInitialize
> mkdir nextjs_deckgl_example > cd nextjs_deckgl_example > yarn init nextjs_deckgl_example -yNext.jsのインストール
必要最低限のライブラリをインストールします。
> yarn add next react react-dom
必要なフォルダを作ります。
> mkdir public src/pages src/componentsTypeScriptを使いたいのでTypeScriptと型定義をインストールします。
> yarn add -D typescript @types/node @types/react @types/react-dom確認のために
src/pages/index.tsx
を作ります。src/pages/index.tsximport { NextPage } from "next"; const Page: NextPage = () => <h1>はろーわーるど!こんにちは Next.js!</h1>; export default Page;
package.json
のscripts
をNext.js用に編集します。package.json{ "scripts": { "dev": "next", "build": "next build", "start": "next start" } }
yarn dev
で開発用サーバが立ち上がり http://localhost:3000 ではろーわーるど!こんにちは Next.js!
と表示されることを確認します。この時にNext.jsがTypeScriptを検出して
tsconfig.json
を作ってくれるので、"strict": false,
を"strict": true,
に変更します。ESLint と Prettierの初期設定
PrettierとESLintとそのLintルールをインストールします
> yarn add -D eslint prettier eslint-plugin-react eslint-config-prettier eslint-plugin-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-airbnb
.eslintrc.json
を作成します。.eslintrc.json{ "extends": [ "airbnb", "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:prettier/recommended", "prettier/@typescript-eslint" ], "plugins": [ "@typescript-eslint", "react" ], "parser": "@typescript-eslint/parser", "env": { "browser": true, "node": true, "es6": true }, "parserOptions": { "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "rules": { "react/prop-types": "off", "react/react-in-jsx-scope": "off", "@typescript-eslint/no-explicit-any": "off" } }TypeScriptの設定をする
importが相対パスだと気持ち悪いので設定を変更します。
これをimport Hello from "../../../components/Hello";こうしたいので
import Hello from "~/components/Hello";
tsconfig.json
をこうします
(pathとbaseUrlはこの設定のためですが、typeRootsに関してはこの後のDeckGLの設定のためです)tsconfig.json{ "compilerOptions": { "baseUrl": "./", "paths": { "~/*": ["src/*"] }, "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx" ], "typeRoots": ["./types", "./node_modules/@types"], "exclude": [ "node_modules" ] }
next.config.js
も同様に記述します。next.config.jsconst path = require("path"); module.exports = { webpack: config => { config.resolve.alias = { ...config.resolve.alias, "~": path.resolve(__dirname, "./src") }; return config; }, };正しく動いているかの確認のためにコンポーネント作って、importしてみます
src/components/Test.tsximport React from "react"; const Test: React.FC = () => <h1>てすとくん</h1>; export default Test;src/pages/index.tsximport { NextPage } from "next"; import Test from "~/components/Test"; const Page: NextPage = () => <Test />; export default Page;.envの設定
開発時は
.env
を使いたいのでdotenv-webpack
を使えるようにします。> yarn add -D dotenv-webpackそれに伴い
next.config.js
も修正しますnext.config.jsconst path = require("path"); const Dotenv = require("dotenv-webpack"); module.exports = { webpack: config => { config.resolve.alias = { ...config.resolve.alias, "~": path.resolve(__dirname, "./src") }; config.plugins = [ ...config.plugins, // 環境変数を優先して読み込む new Dotenv({ systemvars: true }) ]; return config; } };.gitignoreの作成
gitignoreないことに気づいたので作ります
.gitignore.idea # next.js build output .next # dotenv environment variables file .env # Dependency directories node_modules/ # Logs npm-debug.log* yarn-debug.log* yarn-error.log*やっとこれでNext.jsの初期設定が終わりです。
Deck.GLの初期設定
Deck.GLとreact-map-glをインストールする
> yarn add deck.gl react-map-gl > yarn add -D @danmarshall/deckgl-typings @types/react-map-glyarnを使っていると
@danmarshall/deckgl-typings
がバグってしまうことがあるので、deck.d.ts
を追加します> mkdir typestypes/deck.d.tsimport * as DeckTypings from '@danmarshall/deckgl-typings'; declare module 'deck.gl' { export namespace DeckTypings {} }これでDeck.GLの初期設定は終わりです
react-map-glとNext.jsの動作確認をする
_document.tsxを設定する
画面全体にマップを表示したいので、
_document.tsx
でhtml
とbody
のmargin
とpadding
を0にします。_document.tsximport Document, {DocumentContext, Head, Html, Main, NextScript} from 'next/document' export default class MyDocument extends Document { static async getInitialProps(ctx: DocumentContext) { const initialProps = await Document.getInitialProps(ctx) return {...initialProps} } render() { return ( <Html> <Head> <style>{`html,body { margin: 0; padding: 0; }`}</style> </Head> <body> <Main /> <NextScript /> </body> </Html> ) } }react-map-glを試すための準備
Deck.GLを試す前に、
react-map-gl
の表示テストをしましょう。
index.tsx
をreact-map-gl
が表示されるように編集します。
せっかくなのでMaptilerを使いましょう!https://maptiler.jp の右上の
Sign in
からログインをします。
その後、Mapsをクリックし
Japan
にあるJP MIERUNE Streets
を選択して
Use Vector StyleにあるURLをどこか(メモ帳とか)にメモしておきます。
(keyを使うのでそれ以降だけでもOKです。)https://account.mapbox.com からAccess Tokenを取得してどこかにメモしておきます
.env
にアクセストークンを書きます(本番環境では環境変数にしましょう)
空欄にしているのでそこにそれぞれを張り付けてください.env# MapBoxのToken MAPBOX_ACCESSS_TOKEN = # MaptilerのKey MAPTILER_ACCESS_TOKEN =![Something went wrong]()react-map-glを試す
いよいよreact-map-glで実際に表示してみます。
src/pages/index.tsx
を下のコードに帰ると東京周辺のマップが表示されて、マウスでぐりぐり動かせるはずです!src/pages/index.tsximport { NextPage } from "next"; import ReactMapGL from 'react-map-gl'; import {useState} from "react"; const Page: NextPage = () => { const [viewport, setViewport] = useState({ width: 400, height: 400, latitude: 35.681236, longitude: 139.767125, zoom: 10 }); return ( <ReactMapGL {...viewport} width={"100vw"} height={"100vh"} mapStyle={'https://api.maptiler.com/maps/jp-mierune-dark/style.json?key=' + process.env.MAPTILER_ACCESS_TOKEN} mapboxApiAccessToken={process.env.MAPBOX_ACCESSS_TOKEN} onViewportChange={nextViewport => setViewport(nextViewport)} /> ) } export default Page;さて、ここで動作の確認ができたらそろそろ最初の目的である「Next.js+Deck.glで行政区域データ(市区町村の境界のデータ)を表示してみる」をやりましょう!
Next.js+Deck.glで行政区域データ(市区町村の境界のデータ)を表示してみる
データを用意する
国土数値情報 から、データをダウンロードします。
東京の平成31年のデータ(N03-190101_13_GML.zip
)をダウンロードします。
zipを解凍してから、N03-19_13_190101.geojson
をdata.json
にリネームして、src
内にdatas
フォルダを作り入れておきます。Deck.GL用にコードを変更する
上で用意したデータがもとは
geojson
形式なので、GeoJsonLayer
として読みこみます。
それぞれの表示色をランダムにしたいのでgetFillColorでランダムにしています。import { NextPage } from "next"; import {StaticMap} from 'react-map-gl'; import {useState} from "react"; import {GeoJsonLayer} from "@deck.gl/layers"; import {DeckGL} from "deck.gl"; const Page: NextPage = () => { const data = require('~/datas/data.json') const [viewport, setViewport] = useState({ width: 400, height: 400, latitude: 35.681236, longitude: 139.767125, zoom: 10 }); const layer = new GeoJsonLayer({ data, filled: true, stroked: true, getLineWidth: 10, getLineColor: [255, 0, 0], getFillColor: () => { const rand = Math.floor(Math.random() * Math.floor(10)) if (rand <= 5) { return [0, 0, 255] } return [255, 255, 255, 50] } }) return ( <DeckGL width={"100vw"} height={"100vh"} controller layers={[layer]} viewState={viewport} onViewStateChange={(viewState) => setViewport(viewState.viewState)} > <StaticMap width={100} height={100} mapStyle={'https://api.maptiler.com/maps/jp-mierune-dark/style.json?key=' + process.env.MAPTILER_ACCESS_TOKEN} mapboxApiAccessToken={process.env.MAPBOX_ACCESSS_TOKEN} /> </DeckGL> ) } export default Page;下のように表示されたら終了です!
(毎回ランダムなので色の場所は変わります!)
さいごに
思っていた数十倍すんなり動いて感激してます。
通常のcreate-react-appで作ったものよりもカスタマイズがしやすく、ESLintとかPrettierとかの設定がしやすい印象でした。
create-react-appで作ったものよりも動作も心なしかサクサクだったようなので、こっちを使っていこうかなという感じです。全国でやってみるとかは別な記事で....
読んでいただきありがとうございました!ノシ
- 投稿日:2020-12-08T00:18:58+09:00
React Testing Libraryを使ってReactアプリをテストする方法
React Testing Libraryとは?
React Testing Libraryは、Reactコンポーネントをテストするための非常に便利なソリューションです。軽量で便利なAPIを提供してくれていて、Reactサイトのドキュメントを開くと、テストの項目中で推奨ツールとして使われていることがわかります。
基础知识
it
/test
: テスト自体を記述するために使用され、2つの引数が含まれています。1つ目はそのテストの説明であり、2つ目はそのテストを実行する関数です。
expect
: テストに合格する必要がある条件を示し、自体のパラメータをmatcher
と比較します。
matcher
: 目的の条件に到達することが期待される関数です。
render
: 与えられたコンポーネントをレンダリングするために使用される関数です。例えば、
it
を使ってテストを記述し、render
関数を使ってAppコンポーネントをレンダリングし、`asFragment(<App />)
の結果がtoMatchSnapshot()
というmatcher
に合うことを期待しています。import React from "react"; import { render } from "@testing-library/react"; import App from "./App"; it("snapshot test", () => { const { asFragment } = render(<App />); expect(asFragment(<App />)).toMatchSnapshot(); });一般的な使用方法の例
1. テストスナップショットの作成
スナップショットでは、指定されたコンポーネントのスナップショットを保存することができる。コンポーネントの更新やリファクタリングを行い、変更点を取得したり比較したりしたいときに非常に便利です。
現在、App.jsのスナップショットテストをしてみましょう。
render
を使ってAppコンポーネントをレンダリングし、その関数からasFragment
を返すことができます。最後に、コンポーネントのフラグメントがスナップショットと一致していることを確認します。import React from "react"; import { render, cleanup } from "@testing-library/react"; import App from "./App"; // メモリリークを避けるために、各テスト後にすべてのコンテンツを消去する afterEach(cleanup); it("should take a snapshot", () => { // コンポーネントをレンダリングする const { asFragment } = render(<App />); expect(asFragment(<App />)).toMatchSnapshot(); });2.DOM要素のテスト
まず、テストしたいコンポーネントを作ります。
// TestElements.jsx import React from "react"; const TestElements = () => { const [counter, setCounter] = React.useState(0); return ( <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={() => setCounter(counter + 1)}>Up</button> <button disabled data-testid="button-down" onClick={() => setCounter(counter - 1)} > Down </button> </> ); }; export default TestElements;気をつけなければならないのは、data-testidです。 これはテストコードからdom要素を取得するために使用されます。
では、テストを書いてみましょう。1.Counterが
0
に等しいかどうかをテストします。// TestElements1.test.jsx import React from "react"; import { render, cleanup } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import TestElements from "./TestElements"; afterEach(cleanup); it("Counter should equal to 0", () => { const { getByTestId } = render(<TestElements />); expect(getByTestId("counter")).toHaveTextContent(0); });2.ボタンが無効になっているか、有効になっているかをテストします。
// TestElements2.test.jsx import React from "react"; import { render, cleanup } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import TestElements from "./TestElements"; afterEach(cleanup); it("Button should be enabled", () => { const { getByTestId } = render(<TestElements />); expect(getByTestId("button-up")).not.toHaveAttribute("disabled"); }); it("Button should be disabled", () => { const { getByTestId } = render(<TestElements />); expect(getByTestId("button-down")).toBeDisabled(); });3.イベントのテスト
まず、テストしたいコンポーネントを作ります。
// TestEvents.jsx import React from "react"; const TestEvents = () => { const [counter, setCounter] = React.useState(0); return ( <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up </button> <button data-testid="button-down" onClick={() => setCounter(counter - 1)}> Down </button> </> ); }; export default TestEvents;そして、テストを書いてみましょう。
1.ボタンをクリックするだけでcounterが正しく+1、-1されることをテストします。
ここでfireEvent.click()でクリックのイベントをトリガーします。fireEvent
には、イベントをテストするためのいくつかの関数があります。// TestEvents.test.jsx import React from "react"; import { render, cleanup, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import TestEvents from "./TestEvents"; afterEach(cleanup); it("increments counter", () => { const { getByTestId } = render(<TestEvents />); // fireEvent.click()でクリックのイベントをトリガーする fireEvent.click(getByTestId("button-up")); expect(getByTestId("counter")).toHaveTextContent("1"); }); it("decrements counter", () => { const { getByTestId } = render(<TestEvents />); // fireEvent.click()でクリックのイベントをトリガーする fireEvent.click(getByTestId("button-down")); expect(getByTestId("counter")).toHaveTextContent("-1"); });4.非同期操作のテスト
まず、テストしたいコンポーネントを作ります。
ここでsetTimeout()
を使用して、非同期操作をシミュレートします。// TestAsync.jsx import React from "react"; const TestAsync = () => { const [counter, setCounter] = React.useState(0); const delayCount = () => setTimeout(() => { setCounter(counter + 1); }, 500); return ( <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={delayCount}> Up </button> <button data-testid="button-down" onClick={() => setCounter(counter - 1)}> Down </button> </> ); }; export default TestAsync;そして、テストを書いてみましょう。
まず、非同期操作を処理するためにasync/await
を使用しなければなりません。 次に、新しい関数getByText()
を使用します。これはgetByTestId()
に似ていますが、以前使用していたtest-id
ではなく、テキストの内容によってdom要素を取得できます。// TestAsync.test.jsx import React from "react"; import { render, cleanup, fireEvent, waitForElement, } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import TestAsync from "./TestAsync"; afterEach(cleanup); it("increments counter after 0.5s", async () => { const { getByTestId, getByText } = render(<TestAsync />); fireEvent.click(getByTestId("button-up")); const counter = await waitForElement(() => getByText("1")); expect(counter).toHaveTextContent("1"); });5.React Reduxのテスト
まず、テストしたいコンポーネントを作ります。
// TestRedux.jsx import React from "react"; import { connect } from "react-redux"; const TestRedux = ({ counter, dispatch }) => { const increment = () => dispatch({ type: "INCREMENT" }); const decrement = () => dispatch({ type: "DECREMENT" }); return ( <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={increment}> Up </button> <button data-testid="button-down" onClick={decrement}> Down </button> </> ); }; export default connect((state) => ({ counter: state.count }))(TestRedux);// store/reducer.js export const initialState = { count: 0, }; export function reducer(state = initialState, action) { switch (action.type) { case "INCREMENT": return { count: state.count + 1, }; case "DECREMENT": return { count: state.count - 1, }; default: return state; } }そして、テストを書いてみましょう。
1.初期状態が0に等しいかどうかをテストする
ここでは、コンポーネントをレンダリングするための独自のヘルパー関数renderWithRedux()
を作成しました。renderWithRedux()
は、レンダリングするコンポーネント、initialState
、store
を引数として受け取ります。store
がない場合は新しいstore
を作成し、initialState
またはstore
を受信しなかった場合は空のオブジェクトを返します。次にrender()
を使用してコンポーネントをレンダリングし、store
をProvider
に渡します。// TestRedux1.test.jsx import React from "react"; import { createStore } from "redux"; import { Provider } from "react-redux"; import { render, cleanup, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { initialState, reducer } from "../store/reducer"; import TestRedux from "./TestRedux"; const renderWithRedux = ( component, { initialState, store = createStore(reducer, initialState) } = {} ) => { return { ...render(<Provider store={store}>{component}</Provider>), store, }; }; afterEach(cleanup); it("checks initial state is equal to 0", () => { const { getByTestId } = renderWithRedux(<TestRedux />); expect(getByTestId("counter")).toHaveTextContent("0"); });2.Counterの+1と-1が正しいかどうかのテスト
+1と-1のイベントをテストするために、2番目のパラメータとしてinitialState
をrenderWithRedux()
に渡します。// TestRedux2.test.jsx import React from "react"; import { createStore } from "redux"; import { Provider } from "react-redux"; import { render, cleanup, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { initialState, reducer } from "../store/reducer"; import TestRedux from "./TestRedux"; const renderWithRedux = ( component, { initialState, store = createStore(reducer, initialState) } = {} ) => { return { ...render(<Provider store={store}>{component}</Provider>), store, }; }; afterEach(cleanup); it("increments the counter through redux", () => { const { getByTestId } = renderWithRedux(<TestRedux />, { initialState: { count: 5 }, }); fireEvent.click(getByTestId("button-up")); expect(getByTestId("counter")).toHaveTextContent("6"); }); it("decrements the counter through redux", () => { const { getByTestId } = renderWithRedux(<TestRedux />, { initialState: { count: 100 }, }); fireEvent.click(getByTestId("button-down")); expect(getByTestId("counter")).toHaveTextContent("99"); });6.React Routerのテスト
まず、テストしたいコンポーネントを作ります。
// TestRouter.jsx import React from "react"; import { Link, Route, Switch, useParams } from "react-router-dom"; const About = () => <h1>About page</h1>; const Home = () => <h1>Home page</h1>; const Contact = () => { const { name } = useParams(); return <h1 data-testid="contact-name">{name}</h1>; }; const TestRouter = () => { const name = "TEST"; return ( <> <nav data-testid="navbar"> <Link data-testid="home-link" to="/"> Home </Link> <Link data-testid="about-link" to="/about"> About </Link> <Link data-testid="contact-link" to={`/contact/${name}`}> Contact </Link> </nav> <Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/about:name" component={Contact} /> </Switch> </> ); }; export default TestRouter;そして、テストを書いてみましょう。
1.ルートの切り替え時に、正しくレンダリングされるかどうかをテストする
React Router
をテストするには、まずhistory
が必要なので、createMemoryHistory()
を使ってhistory
を作成します。次に、ヘルパー関数renderWithRouter()
を使用してコンポーネントをレンダリングし、history
をRouterコンポーネントに渡します。これで、最初に読み込んだページがホームページであるかどうかをテストし、期待されるLinkコンポーネントをナビゲーションバーに表示することができるようになりました。// TestRouter1.test.jsx import React from "react"; import { Router } from "react-router-dom"; import { render, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { createMemoryHistory } from "history"; import TestRouter from "./TestRouter"; // Helper function const renderWithRouter = (component) => { const history = createMemoryHistory(); return { ...render(<Router history={history}>{component}</Router>), }; }; it("should render the home page", () => { const { container, getByTestId } = renderWithRouter(<TestRouter />); const navbar = getByTestId("navbar"); const link = getByTestId("home-link"); expect(container.innerHTML).toMatch("Home page"); expect(navbar).toContainElement(link); });2.Linkをクリックすると、別のページに移動するかどうかをテストする
Linkが動作しているかどうかを確認するには、Linkのクリックイベントを発生させる必要があります。// TestRouter2.test.jsx import React from "react"; import { Router } from "react-router-dom"; import { render, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { createMemoryHistory } from "history"; import TestRouter from "./TestRouter"; // Helper function const renderWithRouter = (component) => { const history = createMemoryHistory(); return { ...render(<Router history={history}>{component}</Router>), }; }; it("should navigate to the about page", () => { const { container, getByTestId } = renderWithRouter(<TestRouter />); fireEvent.click(getByTestId("about-link")); expect(container.innerHTML).toMatch("About page"); }); it("should navigate to the contact page with the params", () => { const { container, getByTestId } = renderWithRouter(<TestRouter />); fireEvent.click(getByTestId("contact-link")); expect(container.innerHTML).toMatch("TEST"); });7.HTTP Requestのテスト
まず、テストしたいコンポーネントを作ります。
簡単なコンポーネントにリクエストボタンを実装します。 そして、データが利用できない場合は、メッセージ(Loading...)
を表示します。// TestAxios.jsx import React from "react"; import axios from "axios"; const TestAxios = ({ url }) => { const [data, setData] = React.useState(); const fetchData = async () => { const response = await axios.get(url); setData(response.data.greeting); }; return ( <> <button onClick={fetchData} data-testid="fetch-data"> Load Data </button> {data ? ( <div data-testid="show-data">{data}</div> ) : ( <h1 data-testid="loading">Loading...</h1> )} </> ); }; export default TestAxios;そして、テストを書いてみましょう。
1.データが正しく取得され、表示されていることをテストする
HTTPのリクエストを処理するためには、jest.mock('axios')
でaxios
リクエストをシミュレートする必要があります。あとはaxiosMock
のget()
関数とJestの組み込み関数mockResolvedValueOnce()
を使って、シミュレートされたデータを引数として渡します。2番目のテストでは、非同期リクエストを処理するためにasync/await
を使用しなければなリません。// TestAxios.test.jsx import React from "react"; import { render, waitForElement, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import axiosMock from "axios"; import TestAxios from "./TestAxios"; jest.mock("axios"); it("should display a loading text", () => { const { getByTestId } = render(<TestAxios />); expect(getByTestId("loading")).toHaveTextContent("Loading..."); }); it("should load and display the data", async () => { const url = "/greeting"; const { getByTestId } = render(<TestAxios url={url} />); axiosMock.get.mockResolvedValueOnce({ data: { greeting: "hello there" }, }); fireEvent.click(getByTestId("fetch-data")); const greetingData = await waitForElement(() => getByTestId("show-data")); expect(axiosMock.get).toHaveBeenCalledTimes(1); expect(axiosMock.get).toHaveBeenCalledWith(url); expect(greetingData).toHaveTextContent("hello there"); });終わりに
ここまでで、簡単な7つのステップでほどんとReactアプリをテストできました。
React Testing Libraryを使えば、Reactアプリを完全にテストしやすくなり、テストを書くのが楽しくなり、もうコンポーネントを気にする必要がない生活を享受することができるようになります。