20210906のReactに関する記事は15件です。

リアルタイムなWebアプリを簡単に作れるOSSを作ったので、立ち上げからリリースまでを時系列に書いていく

まえがき ちょうど3年前に「初心者3人でwebサービス(webアプリ)を作ったので、立ち上げからリリースまでを時系列に書いていく」なる記事を書いたことがあり、いまだにいいねをつけてもらえることがあります。そこで、3年の時を経て似たような記事を書いてみようと思いました。 Webアプリの開発経験はあるものの、npmパッケージを公開したことはありませんでした。手探りからどのようにリリースまで、残したドキュメントをもとに振り返っていきたいと思います。 これを読んで「OSSを作ってみたいけどどう進めていけばいいか分からない」という人の一助になれば嬉しいです。誰か0から10までの流れを書いててくれないかな〜と思っていたので、きっと誰かには届く気がします。 作ったOSSについて Realtimelyという、Reactアプリに楽しいリアルタイム要素を簡単に導入できるライブラリを開発しました。 Realtimelyの裏側ではGraphQLとWebsocketでAWSクラウドと通信していますが、プログラマはそれを意識することなくRealtimelyの提供するhooks関数を使うだけでwebsocketベースのリアルタイムな体験を実装することができるようになります。 例えば、下のgifのようにマウスカーソルの位置を共有する機能を10行程度のコードで実現できます。 import { useRealtimeCursor } from 'realtimely'; export default () => { const { onMouseMove, renderCursors } = useRealtimeCursor() return ( <div onMouseMove={onMouseMove}> {renderCursors()} </div> ) } デモページを作っているので、よかったら触ってみてください。 立ち上げからリリースまで さて、ここからは実際にどのように企画を立ててリリースしていったかを時系列に書いていきたいと思います。 人数 1人 制作日数 18日 かかったお金 AWS無料枠 1日にかけた時間 平日1時間 休日3時間 8/19(開始から0日目) 計画 初日に決めたことは以下になります。 ユースケース → どのような場面でこのライブラリを使うのかイメージ プロダクトイメージ → プログラマがどのようなインターフェイスでこのライブラリを使うかをイメージ アーキテクチャ → 何を使って実現するか。 今回は可能な限り(楽をするため)ローコードでスケーラブルにしたいという思いがありました。 必要なタスク洗い出しとスケジュール 何をやらないといけないか列挙 何にどれくらい時間がかかりそうか見積もり(正直個人開発で見積もりすることに意味はないですが、どれくらいかかりそうか把握するためにやりました) 今回はかなりプロダクトアウト的な発想でユースケースを検討しましたが、実際には以下の手順でやるのが理想かと思います。 ① 実際の開発で困っている課題を考える ② 誰かがすでに実現していないか調べる ③ 実現していたとして、それが本当に便利か、より使いやすいプロダクトイメージがないか考える OSSは誰にでも公開できますが、すでに実現されていたり、さほど困っていないことをベースにしたものを作って公開しても検索の邪魔になるプロダクトができてしまう恐れがあります。 8/20 (開始から1日目) 競合(?)調査 自分がやりたいことをやれそうなライブラリを調べ、ドキュメントを読み、実際に使ってみました。 そのライブラリは「パフォーマンスの高いリアルタイムアプリ」を作るためにRESTベースで作られたライブラリでした。 高いパフォーマンスが目的だったのでキャッシュ等を利用した状態管理も機能の中に入っておりtoo muchであることや、バックエンドもフルスクラッチで開発しなければならず簡単に使えるものではないことがわかりました。 ここで自分のプロダクトイメージを手軽に使えるようにするところに尖らせるなど方向修正したりしました。 また方向性は違えど、実現することは似ているので参考にできることがたくさんあります。HTTPレスポンスなども見て、どういったデータ構造が良さそうかなどの参考にしました。 8/21-8/26 (開始から6日目) First iteration 「リアルタイムにカーソルの動きを共有する」というスプリントゴールを最初の7日で実装しました。 AWS AppSyncを触る AppSyncはGraphQLサーバのマネージドサービスです。 DynamoDBのテーブルと接続することで、自動でGraphQLスキーマとリゾルバを生成する機能があります。 今回はローコードでなるべく楽しようと考えていたので、AppSyncを採用してみました。 ノーコード、ローコード系あるあるですが、どうしても限界というかサービスの特徴があり、そこからはみ出すと一気に実装が難しくなります。AppSyncもその例に漏れず、AppSyncの特徴を捉えながら設計をする必要がありました。 触ってみて感じたことはこんな感じ。 自分で作ってないものは挙動を理解するのに時間がかかる → DynamoDBを手動で更新してもSubscriptionが更新してくれなくてハテナとなった → DynamoDBを監視しているわけではなくMutaitonを監視していた カスタムロジックを書こうとしたらVTLを書かないといけない → 基本的なコードはネットから拾えるが、自分で一からVTLでロジックを書くのはしんどすぎる できることの範囲に限界があり、追加要件が走った瞬間に破綻するのではないかという不安から実務で採用は難しそう。 自動生成されたリゾルバに勝手にロジックが書かれていてハマる → 自動生成系は何が実装されているか分からない 不可思議な挙動にも身体で慣れていくしかない。このノウハウにあまり再利用性がない。 AppSyncでweb socket通信をするためのハンドシェイクプロトコルが独特 Subscriptionの挙動が奇妙(StackOverflow) 設計する 「リアルタイムにカーソルを移動させる」を実装させるために必要なAPIとフロントでの処理を書き出し、実現できそうか検討していきました。この辺は「実現したいこと」と「AppSyncでできること」を両睨みして考えていく形でした。 ユーザがURL(Host + Path)にアクセス 誰かが入室したら通知(onCreateRealtimeUser) 誰かの所在更新を通知(onUpdateRealtimeUser) → 所在が古いものはローカルで削除する そのURLへアクセスしていることを報告(CreateRealtimeUser) カーソルの移動に合わせて位置を報告(createRealtimeCursor, updateRealtimeCursor) 他の人がカーソル移動したら通知(onUpdateRealtimeCursor) 自分が存在していることをpoke (updateRealtimeUser) DB設計 DynamoDBはキーバリューストアのスケーラブルなNoSQLデータベースです。 DynamoDBのキーはHashKeyとSortKeyがあり、この組み合わせが一意であるようにDB設計を行います。 RealtimeCursorは次のようなスキーマにしました。 HashKey SortKey URL#{URL} UserId#{UserId} {URL}と{UserId}には実際の値が入ります。 このキーに対して、マウスのカーソル位置(X,Y)やユーザ情報をAttributeとして保存していきます。 高速実装 僕は実装は何よりもまず最初はスピードだけを意識してやります。 どれだけ汚くてもいいので、とにかくやりたいことができればOKです。 なぜかというと綺麗な設計というのは全てを書き終えてからスクラップアンドビルドをするのが一番効率的だと思っているからです。 最初から何が共通化できるかなんて、僕はわかりません。 リファクタリング ざっくりと汚いコードでやりたいことが実現できたらリファクタリングをしました。 Webサービスであれば最悪リファクタリングをしなくても動くものを先にリリースするという判断もありかと思います。 今回はOSSライブラリとして使いやすくなければいけないため、責務とインターフェイスをよく考えて再設計しました。 8/27-8/30 (開始から11日目) Second iteration 「ユーザアクションの可視化を実装する(ボタンを押すと他の人にも押されたように見えるとか、テキストを入力するとそれが他の人の画面に見えるとか)」というスプリントゴールに2回目のイテレーションを回します。 リファクタリングされて整理されているのと、おおよそAppSyncで何ができるかわかっているのでスムーズに開発が進みました。 8/31 (開始から12日目) デモページを作る 開発に使っていたNext.jsプロジェクトを多少整理してそのままデプロイします。 Serverless Frameworkを使うことで5分でAWSにデプロイできました。 (デモページ)[https://d2vfno2gco8009.cloudfront.net/] 9/1-9/4 (開発から16日目) ドキュメントを作る docusaurusというライブラリを使ってドキュメントを作成しました。docusaurusはfacebookがメンテしており、めちゃくちゃよくできてます。 まず、大項目で書くことを列挙してからゴリゴリと書いていきました。 書くことを考える Introduction Getting Started Installation Demo Realtime Cursor Realtime User Presence Realtime User Action API Guides useXXX How it works Architecture Self hosted Backend RoadMap ゴリゴリ書く ゴリゴリ書きます。一番しんどかったところ。 英語にする docusaurusにi18n機能があるので、英語のドキュメントも作成しました。 基本的にgoogle translateに翻訳させて、ざっと見直しするだけです。 Github Pagesにデプロイする これもdocusaurusに機能としてあったので簡単にできました。 その他のタスク ロゴを作る これは適当にロゴgeneratorを活用しました。 寄付の窓口を作る ko-fiを使って寄付の窓口も作ってみました。 Stripeと接続してクレジットカードで寄付ができるようになります。 9/5-6 (開発から18日目) npm公開 いよいよ公開です。詳細なnpmへの公開方法は別の記事を参照ください。今回は荒く書いていきます。 npmアカウントの作成 https://www.npmjs.com/ でアカウントを作成 TypeScriptをコンパイルして.jsファイルにする tsconfig.jsonを書いてから tsc -d yarn publish npmのログインを求められるのでEmail, Passwordを入力するだけ、、 公開 これでnpmに公開することができました。 公開後の姿 めちゃくちゃ簡単でびびります。npmのライブラリって今まで利用させて頂くばかりで、その土俵に僕なんかがあがっていいのか不安になるような、土俵に汚い土足で乗り上げたような、そんな罪悪感がありました。 デバッグ 意気揚々と別リポジトリでrealtimelyをインストールして使ってみると、エラーが出て使えない・・・ この辺のデバッグで1日溶けました。 ライブラリの依存関係 CSS読み込めないエラー 依存しているライブラリのエラー これから 公開へのスピード優先で開発を進めたので、現在は到底Production Readyなライブラリになっていません。あくまでもPoCレベルで、アプリにリアルタイム要素を入れるとどんな感じになるのか確かめて頂く用途で使っていただければと思います。 これからはProduction Readyにするための課題の解決と、より使いやすい機能の開発を細々とやっていこうと思います。温かい目で見守ってください。 Document Demo
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OSSのライブラリを作ったので、立ち上げからリリースまでを時系列に書いていく

まえがき ちょうど3年前に「初心者3人でwebサービス(webアプリ)を作ったので、立ち上げからリリースまでを時系列に書いていく」なる記事を書いたことがあり、いまだにいいねをつけてもらえることがあります。そこで、3年の時を経て似たような記事を書いてみようと思いました。 Webアプリの開発経験はあるものの、npmパッケージを公開したことはありませんでした。手探りからどのようにリリースまで、残したドキュメントをもとに振り返っていきたいと思います。 これを読んで「OSSを作ってみたいけどどう進めていけばいいか分からない」という人の一助になれば嬉しいです。誰か0から10までの流れを書いててくれないかな〜と思っていたので、きっと誰かには届く気がします。 作ったOSSについて Realtimelyという、Reactアプリに楽しいリアルタイム要素を簡単に導入できるライブラリを開発しました。 Realtimelyの裏側ではGraphQLとWebsocketでAWSクラウドと通信していますが、プログラマはそれを意識することなくRealtimelyの提供するhooks関数を使うだけでwebsocketベースのリアルタイムな体験を実装することができるようになります。 例えば、下のgifのようにマウスカーソルの位置を共有する機能を10行程度のコードで実現できます。 import { useRealtimeCursor } from 'realtimely'; export default () => { const { onMouseMove, renderCursors } = useRealtimeCursor() return ( <div onMouseMove={onMouseMove}> {renderCursors()} </div> ) } デモページを作っているので、よかったら触ってみてください。 立ち上げからリリースまで さて、ここからは実際にどのように企画を立ててリリースしていったかを時系列に書いていきたいと思います。 人数 1人 制作日数 18日 かかったお金 AWS無料枠 1日にかけた時間 平日1時間 休日3時間 8/19(開始から0日目) 計画 初日に決めたことは以下になります。 ユースケース → どのような場面でこのライブラリを使うのかイメージ プロダクトイメージ → プログラマがどのようなインターフェイスでこのライブラリを使うかをイメージ アーキテクチャ → 何を使って実現するか。 今回は可能な限り(楽をするため)ローコードでスケーラブルにしたいという思いがありました。 必要なタスク洗い出しとスケジュール 何をやらないといけないか列挙 何にどれくらい時間がかかりそうか見積もり(正直個人開発で見積もりすることに意味はないですが、どれくらいかかりそうか把握するためにやりました) 今回はかなりプロダクトアウト的な発想でユースケースを検討しましたが、実際には以下の手順でやるのが理想かと思います。 ① 実際の開発で困っている課題を考える ② 誰かがすでに実現していないか調べる ③ 実現していたとして、それが本当に便利か、より使いやすいプロダクトイメージがないか考える OSSは誰にでも公開できますが、すでに実現されていたり、さほど困っていないことをベースにしたものを作って公開しても検索の邪魔になるプロダクトができてしまう恐れがあります。 8/20 (開始から1日目) 競合(?)調査 自分がやりたいことをやれそうなライブラリを調べ、ドキュメントを読み、実際に使ってみました。 そのライブラリは「パフォーマンスの高いリアルタイムアプリ」を作るためにRESTベースで作られたライブラリでした。 高いパフォーマンスが目的だったのでキャッシュ等を利用した状態管理も機能の中に入っておりtoo muchであることや、バックエンドもフルスクラッチで開発しなければならず簡単に使えるものではないことがわかりました。 ここで自分のプロダクトイメージを手軽に使えるようにするところに尖らせるなど方向修正したりしました。 また方向性は違えど、実現することは似ているので参考にできることがたくさんあります。HTTPレスポンスなども見て、どういったデータ構造が良さそうかなどの参考にしました。 8/21-8/26 (開始から6日目) First iteration 「リアルタイムにカーソルの動きを共有する」というスプリントゴールを最初の7日で実装しました。 AWS AppSyncを触る AppSyncはGraphQLサーバのマネージドサービスです。 DynamoDBのテーブルと接続することで、自動でGraphQLスキーマとリゾルバを生成する機能があります。 今回はローコードでなるべく楽しようと考えていたので、AppSyncを採用してみました。 ノーコード、ローコード系あるあるですが、どうしても限界というかサービスの特徴があり、そこからはみ出すと一気に実装が難しくなります。AppSyncもその例に漏れず、AppSyncの特徴を捉えながら設計をする必要がありました。 触ってみて感じたことはこんな感じ。 自分で作ってないものは挙動を理解するのに時間がかかる → DynamoDBを手動で更新してもSubscriptionが更新してくれなくてハテナとなった → DynamoDBを監視しているわけではなくMutaitonを監視していた カスタムロジックを書こうとしたらVTLを書かないといけない → 基本的なコードはネットから拾えるが、自分で一からVTLでロジックを書くのはしんどすぎる できることの範囲に限界があり、追加要件が走った瞬間に破綻するのではないかという不安から実務で採用は難しそう。 自動生成されたリゾルバに勝手にロジックが書かれていてハマる → 自動生成系は何が実装されているか分からない 不可思議な挙動にも身体で慣れていくしかない。このノウハウにあまり再利用性がない。 AppSyncでweb socket通信をするためのハンドシェイクプロトコルが独特 Subscriptionの挙動が奇妙(StackOverflow) 設計する 「リアルタイムにカーソルを移動させる」を実装させるために必要なAPIとフロントでの処理を書き出し、実現できそうか検討していきました。この辺は「実現したいこと」と「AppSyncでできること」を両睨みして考えていく形でした。 ユーザがURL(Host + Path)にアクセス 誰かが入室したら通知(onCreateRealtimeUser) 誰かの所在更新を通知(onUpdateRealtimeUser) → 所在が古いものはローカルで削除する そのURLへアクセスしていることを報告(CreateRealtimeUser) カーソルの移動に合わせて位置を報告(createRealtimeCursor, updateRealtimeCursor) 他の人がカーソル移動したら通知(onUpdateRealtimeCursor) 自分が存在していることをpoke (updateRealtimeUser) DB設計 DynamoDBはキーバリューストアのスケーラブルなNoSQLデータベースです。 DynamoDBのキーはHashKeyとSortKeyがあり、この組み合わせが一意であるようにDB設計を行います。 RealtimeCursorは次のようなスキーマにしました。 HashKey SortKey URL#{URL} UserId#{UserId} {URL}と{UserId}には実際の値が入ります。 このキーに対して、マウスのカーソル位置(X,Y)やユーザ情報をAttributeとして保存していきます。 高速実装 僕は実装は何よりもまず最初はスピードだけを意識してやります。 どれだけ汚くてもいいので、とにかくやりたいことができればOKです。 なぜかというと綺麗な設計というのは全てを書き終えてからスクラップアンドビルドをするのが一番効率的だと思っているからです。 最初から何が共通化できるかなんて、僕はわかりません。 リファクタリング ざっくりと汚いコードでやりたいことが実現できたらリファクタリングをしました。 Webサービスであれば最悪リファクタリングをしなくても動くものを先にリリースするという判断もありかと思います。 今回はOSSライブラリとして使いやすくなければいけないため、責務とインターフェイスをよく考えて再設計しました。 8/27-8/30 (開始から11日目) Second iteration 「ユーザアクションの可視化を実装する(ボタンを押すと他の人にも押されたように見えるとか、テキストを入力するとそれが他の人の画面に見えるとか)」というスプリントゴールに2回目のイテレーションを回します。 リファクタリングされて整理されているのと、おおよそAppSyncで何ができるかわかっているのでスムーズに開発が進みました。 8/31 (開始から12日目) デモページを作る 開発に使っていたNext.jsプロジェクトを多少整理してそのままデプロイします。 Serverless Frameworkを使うことで5分でAWSにデプロイできました。 (デモページ)[https://d2vfno2gco8009.cloudfront.net/] 9/1-9/4 (開発から16日目) ドキュメントを作る docusaurusというライブラリを使ってドキュメントを作成しました。docusaurusはfacebookがメンテしており、めちゃくちゃよくできてます。 まず、大項目で書くことを列挙してからゴリゴリと書いていきました。 書くことを考える Introduction Getting Started Installation Demo Realtime Cursor Realtime User Presence Realtime User Action API Guides useXXX How it works Architecture Self hosted Backend RoadMap ゴリゴリ書く ゴリゴリ書きます。一番しんどかったところ。 英語にする docusaurusにi18n機能があるので、英語のドキュメントも作成しました。 基本的にgoogle translateに翻訳させて、ざっと見直しするだけです。 Github Pagesにデプロイする これもdocusaurusに機能としてあったので簡単にできました。 その他のタスク ロゴを作る これは適当にロゴgeneratorを活用しました。 寄付の窓口を作る ko-fiを使って寄付の窓口も作ってみました。 Stripeと接続してクレジットカードで寄付ができるようになります。 9/5-6 (開発から18日目) npm公開 いよいよ公開です。詳細なnpmへの公開方法は別の記事を参照ください。今回は荒く書いていきます。 npmアカウントの作成 https://www.npmjs.com/ でアカウントを作成 TypeScriptをコンパイルして.jsファイルにする tsconfig.jsonを書いてから tsc -d yarn publish npmのログインを求められるのでEmail, Passwordを入力するだけ、、 公開 これでnpmに公開することができました。 公開後の姿 めちゃくちゃ簡単でびびります。npmのライブラリって今まで利用させて頂くばかりで、その土俵に僕なんかがあがっていいのか不安になるような、土俵に汚い土足で乗り上げたような、そんな罪悪感がありました。 デバッグ 意気揚々と別リポジトリでrealtimelyをインストールして使ってみると、エラーが出て使えない・・・ この辺のデバッグで1日溶けました。 ライブラリの依存関係 CSS読み込めないエラー 依存しているライブラリのエラー これから 公開へのスピード優先で開発を進めたので、現在は到底Production Readyなライブラリになっていません。あくまでもPoCレベルで、アプリにリアルタイム要素を入れるとどんな感じになるのか確かめて頂く用途で使っていただければと思います。 これからはProduction Readyにするための課題の解決と、より使いやすい機能の開発を細々とやっていこうと思います。温かい目で見守ってください。 Document Demo
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

基礎から学ぶReact/React Hooks学習メモ 3 Reactを試してみよう

Reactを試してみよう HTMLファイルでReactを試してみる 「いいね前」「いいね済」がトグルするボタン <!DOCTYPE html> <html> <head> <title>いいねボタン</title> <meta charset="UTF-8" /> <!-- Reactを読み込む --> <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <!-- babelを読み込むとJSXが使えるようになる --> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> </head> <body> <!-- Reactコンポーネントが表示される --> <div id="likesButtonContainer"></div> <script type="text/babel"> const LikeButton = () => { const [liked, setLiked] = React.useState(false); const toggleLiked = () => setLiked(!liked); return ( <button className="likeButton" onClick={toggleLiked}> {liked ? "いいね済" : "いいね前"} </button> ); }; // ReactがLikeButtonをDOMに変換して、HTMLのDOMコンテナに追加する const domContainer = document.querySelector("#likesButtonContainer"); ReactDOM.render(<LikeButton />, domContainer); </script> </body> </html> 文字列の入力状態を保持するNameInputコンポーネント <!DOCTYPE html> <html> <head> <title>React</title> <meta charset="UTF-8" /> <!-- Reactを読み込む --> <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <!-- babelを読み込むとJSXが使えるようになる --> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> </head> <body> <!-- Reactコンポーネントが表示される --> <div id="App"></div> <script type="text/babel"> const NameInput = () => { const [name, setName] = React.useState("〇〇"); const handleOnChange = (event) => setName(event.target.value); return ( <div> <input type="text" onChange={handleOnChange} /> <p>こんにちは、{name}さん</p> </div> ); }; // ReactがNameInputをDOMに変換して、HTMLのDOMコンテナに追加する const domContainer = document.querySelector("#App"); ReactDOM.render(<NameInput />, domContainer); </script> </body> </html> CodeSandboxでReactを試してみる CodeSandBoxにログイン(https://codesandbox.io/) SandBoxを作成、Reactを選択する。(左下のDependenciedでバージョンの切り替えが可能) カウンタープログラムを作成 import React, { useState } from "react"; import "./styles.css"; // 表示用コンポーネント const CounterText = (props) => <p>現在のカウント数:{props.count}</p>; const INITIAL_COUNT = 0; const Counter = () => { const [count, setCount] = useState(INITIAL_COUNT); // カウントアップ、ダウンボタン。現在の値を引数で受け取ることができる const countAdd = () => setCount((prevCount) => prevCount + 1); const countSub = () => setCount((prevCount) => prevCount - 1); const countDouble = () => setCount((prevCount) => prevCount * 2); const countHalf = () => setCount((prevCount) => prevCount / 2); const countReset = () => setCount(INITIAL_COUNT); return ( <> <CounterText count={count} /> <button onClick={countAdd}>ボタン +1</button> <button onClick={countSub}>ボタン -1</button> <button onClick={countDouble}>ボタン *2</button> <button onClick={countHalf}>ボタン /2</button> <button onClick={countReset}>リセット</button> </> ); }; export default function App() { return <Counter />; } Create React Appについて Node.jsをインストール # yarnのインストール npm install -g yarn # プロジェクト作成 npx create-react-app react-practice # アプリ起動 cd react-practice yarn start Reactの開発を補助するツール node.js JavaScriptをサーバーサイドで動かす npmはJavaScriptのパッケージ管理ツール Babel JSX→JavaScriptに変換 新しい構文をES5のコードに変換する webback 複数のJavaScriptのモジュールをまとめることにより、パフォーマンス向上が期待できる。 Sass→CSSへのコンパイル、画像の圧縮、ESlintなど 参考 - 基礎から学ぶReact/React Hooks
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactをexpressにホストする

やり方を聞かれて調べたのでメモ。 react-router-domでルーティングされているページをbuildしてexpressにホストしたい。 Reactでの作業 とりあえず何でもいいのですが、react-router-domで最低限のルーティングを設定します。 簡易仕様 / → Home.js /about → About.js それ以外だと、Page not found.を表示。 プロジェクトの作成 とりあえずcreate-react-appでプロジェクトを作成。 npx create-react-app react-test cd react-test 最低限のルーティングを設定したいのでreact-router-domのインストール。 npm install react-router-dom Home.jsとAbout.jsを生成。 touch src/Home.s src/About.js Home.js Home.jsの実装。Homeと表示し、About.jsへのリンクがあるだけ。 Home.js import { Link } from "react-router-dom"; const Home = () => { return ( <div> Home<br/> <Link to="/about">Aboutへ</Link> </div> ); } export default Home; About.js Aboutと表示しHome.jsへのリンクがあるだけ。 About.js import { Link } from "react-router-dom"; const About = () => { return ( <div> About<br/> <Link to="/">Homeへ</Link> </div> ); } export default About; App.js ルーティングの設定。/でも/aboutでもなければPage not foundを表示。 App.js import { BrowserRouter, Switch, Route } from "react-router-dom"; import Home from "./Home"; import About from "./About"; const App = () => { return ( <BrowserRouter> <Switch> <Route path="/" exact component={Home} /> <Route path="/about" exact component={About} /> <Route render={()=><p>Page not found.</p>}/> </Switch> </BrowserRouter> ); } export default App; 動作確認 npm startで動作確認。 http://localhost:3000。 npm start build(本番用ファイル生成) 今回の肝はbuildしたファイルがexpressで動くかなのでbuild。 npm run build buildファイルが作成され、中にbuild結果が出力される。index.htmlを始めとする各種ファイルが生成される。 Express側での作業 上記で生成されたファイル群をexpressの静的ファイルホスティングで正常に動作させるのがゴール。 expressのインストールと作業場の作成 expressをインストールし、reactのプロジェクトフォルダ中にnodeフォルダを作る(わけてもいい。めんどいのでそうしているだけ)。そして、index.jsを生成。 npm install express mkdir node touch node/index.js cd node 実装 index.jsを実装していく。 index.js const path = require("path"); const express = require("express"); const app = express(); //ミドルウエアでstaticパスを追加(ただ、これだけだと直アクセスや無いpathだと動かない) app.use(express.static(path.join(__dirname, "..", "build"))); //これを追加(全てをindex.htmlにリダイレクト。いわゆるrewrite設定) app.use((req, res, next) => { res.sendFile(path.join(__dirname, "..", "build", "index.html")); }); app.listen(3001, () => { console.log("server started on port 3001"); }); ポイント 動作のポイントは2つ。 静的ファイルの場所 まず、静的フォルダを設定する。 これだけで実は http://localhsot:3001にアクセスすると動きます。が、これだけでは不十分。 app.use(express.static(path.join(__dirname, "..", "build"))); /を経由せず、ダイレクトに/aboutや/xxxとかにアクセスするとエラーとなる(表示されない or Not foundとならない)。 全てのリクエストをbuild/index.htmlへ これらを処理するには全てのリクエストをindex.htmlに処理させる必要がある。 一般的なWebサーバではRewirte処理などを行うがExpressでは以下のようにする。 app.use((req, res, next) => { res.sendFile(path.join(__dirname, "..", "build", "index.html")); }); 実行 では動作確認してみる。 node index.js 確認
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React+Typescript+Firebase v9

はじめにの前の結論 Firebase ver.9 を使って React で認証を実装した。 ソースコード: kurab/react-typescript-firebase Firebase ver.8 vs ver.9 の薄い解説: Firebase9 注入 はじめに 紆余曲折あって、React の認証機能を Firebase に外出しした。ググれば余裕っしょと高を括っていたが、タイミング良く?悪く?Firebase の Node Module が 8 から 9 にバージョンアップした。ドキュメントを見ると、9はまだベータ版と書いてあるが、npm/yarn して入るのは、今日現在であれば、9.0.1 でそれが Latest となっているので、もう 9 なのだろう。バージョンアップガイドを見ると、 バージョン9の利点と限界 完全にモジュール化されたバージョン9には、以前のバージョンに比べて以下のような利点があります。 バージョン9では、アプリのサイズを大幅に縮小することができます。最新のJavaScriptモジュール形式を採用しており、アプリに必要なアーティファクトだけをインポートする「ツリーシェイク」が可能です。アプリケーションにもよりますが、バージョン9でツリーシェイクを行うと、バージョン8で構築した同等のアプリケーションに比べて、80%のキロバイト削減が可能です。 バージョン9は今後も継続的な機能開発が行われますが、バージョン8は将来の時点で凍結されます。 とあるので、今後は 9 系統を使えば良かろうと判断した。が、いかんせん出たてホヤホヤなので、やってみたドキュメントがない。私のような日本語で教えて君にはつらいタイミングだ。8 の書き方だと、9 では動かない。面倒くさいなと思ったが、ベトナムは4連休だったので、トライしてみた。 作りたいもの こんなようなものを作ることを目指した。割と汎用的な構成ではないかと思う。 最終的にできたものは、この図の Home にログアウトボタンを追加したのと、Facebook で登録ボタンは、Facebook でログインボタンと同じ機能になった。Firebase に Facebook で登録という機能はない(ログインすると登録される)。実際のアプリケーションでは、その他必要な情報を取得することもあるかと思うので、処理が分かれるかも知れないが、今回の例では必要ないので、省略した。 バージョン情報 React: 17.0.2 Firebase: 9.0.1 react-router-dom: 5.3.0 インストール手順は、こんな感じ $ npx create-react-app app --template typescript $ cd app $ yarn add react-route-dom firebase $ yarn add --dev @types/react-router-dom @types/firebase $ npx browserslist@latest --update-db 最後のは、yarn start したらやれと言われたのでやった。 リポジトリのルートディレクトリには、docker 関連等を置きたかったので、一段下げた。 実装方針 素の Router を書き、各ページを作成・表示できるようにする 各ページのデザイン(html+css) Firebase を利用しない簡単な認証機能(メールとパスワードが一致するかだけ)の実装 未認証の場合、ログイン画面にリダイレクトする PrivateRoute の実装 Firebase 注入 認証後、リロード時にログイン画面に戻ってしまう件修正 Facebook ログインが Localhost に厳しい件の暫定対応 その他の方針としては、Type はきっちり明示、CSS FW は使わない、Atomic design の方向で、くらい。 出来上がったものは、こちら ▶ kurab/react-typescript-firebase なお、エラーメッセージ表示などは、ほとんどやってない。alert が出るだけ。+ React.js は経験2〜3ヶ月のぺーペーなので、至らないところはゴメンナサイ。 実装 手順の5以外は、今回の本質ではないので、サラッと。 1. 素の Router を書き、各ページを作成・表示できるようにする Routing の基本的な処理は、 app/src/router/Router.tsx app/src/router/AuthRouter.tsx と app/src/App.tsx で、react-router-dom で実装している。完成形は、素ではなく、PrivateRoute や Provider などが入っている。 各ページは、 app/src/components/pages/* 2. 各ページのデザイン(html+css) 前述の app/src/components/pages/* に必要な要素を配置し、CSS は、app/src/assets/styles/* にて scss で書いた。scss の build? compile? は VS Code 任せ。React っぽい書き方ではないと思うが、個人的にまだピンと来ていないというか、しっくり来るやり方が見つかっていないので、こんな感じで書いている。 実際の画面は、デザインに結構忠実なのでスクリーンショットは省略。 3. Firebase を利用しない簡単な認証機能(メールとパスワードが一致するかだけ)の実装 完成形では消えてしまっているが、 app/src/hooks/useAuth.ts app/src/hooks/useLoginUser.ts app/src/providers/LoginUserProvider.tsx に書いてある(あった)。出来上がった Provider で Router の 404 ページ以外を囲っている。 app/src/types/api/User.ts.bk は、この時点の名残り。なんか上がっちゃってた。useAuth に user 配列をハードコードして if 文書いてただけ。 form の値の取得は、色々なやり方があると思うが、私は ChangeEvent<HTMLInputElement> で取得し、useState を使って利用している。Register 画面のパスワード2回のチェックは、同じにならなければ、Register ボタンを押せないようにするという手抜き実装。細かくエラーメッセージ出したりする場合は、もうちょっと考える必要があるが、今回はこれで。 4. 未認証の場合、ログイン画面にリダイレクトする PrivateRoute の実装 PrivateRoute は、 app/src/router/PrivateRoute.tsx もっとスマートな書き方もあるのかと思うけど、見た目の分かりやすさ優先。それを使って、 app/src/router/Router.tsx を書き換えた。 6. 認証後、リロード時にログイン画面に戻ってしまう件修正 app/src/providers/LoginUserProvider.tsx の useEffect あたりが、それ。 7. Facebook ログインが Localhost に厳しい件の暫定対応 Facebook ログインは、Redirect URI が SSL でないと機能せず、Localhost に厳しい。Firebase チュートリアル記事で Facebook ログインが少ない理由は、これか…な? という訳で、Docker を使って解決した。 Dockerでローカル開発環境のhttps化 この記事そのまま。Chrome のセキュリティを下げることになるが、この記事にある通り、Localhost なので、許容範囲かと思う。必要ないときは、元に戻しておけば良い。 Firebase9 注入 では、いよいよ Firebase9。と言っても、ドキュメントに全部書いてあるので、それ読んでという感じ。 Firebase の設定、Facebook Dev の設定は、 【完全版】React の Firebase Authentication(認証)を基礎からマスターする 【React/TypeScript】Firebase でメール認証と Google 認証を実装する [Firebase] Authentication で Facebook 認証 (Web 編) etc. このあたりを参照した。.env には、Firebase の設定値を書く。 React 内の Firebase の実装は、firebase.js/ts とかを作って…という解説が一般的だが、なんか嫌だったので、認証系処理を行う hook の app/src/hooks/useAuth.ts の中に書いた。 Firebase 8 系と 9 系の違いは、例えばメールとパスワードでユーザ登録する場合、8では、 ver.8 firebase.auth().createUserWithEmailAndPassword(email, password)... こんな感じになるが、9 の場合、 ver.9 import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; const auth = getAuth(); createUserWithEmailAndPassword(auth, email, password)... こんな感じになる。ドットチェインがなくなり、auth = getAuth() して、パラメータとして渡すようになった。全ての関数を比較したわけではないが、概ねこんな感じ。 WebとFirebaseの詳細 にも説明がある。 バージョン9のSDKの大部分は、バージョン8と同じパターンを踏襲していますが、コードの構成は異なります。一般的に、バージョン8は名前空間とサービスのパターンを指向しているのに対し、バージョン9は個別の機能を指向しています。例えば、バージョン8では firebaseApp.auth() のようなドットチェインがありましたが、バージョン9では firebaseApp を受け取り、Authentication インスタンスを返す getAuth() 関数に変更されています。 JavaScript で Facebook ログインを使用して認証する にも、基本的な操作のいくつかの比較があるので参照。 その他のメソッドについては、認証パッケージ に丁寧に書いてあるので、それを見れば解決しないことはないと思われる。 useAuth.ts 内で、useLoginUser から setLoginUser を利用しているが、その実態は、app/src/providers/LoginUserProvider.tsx で定義されている。 LoginUserProvider.tsx export type LoginUserContextType = { loginUser: LoginUser | null; setLoginUser: Dispatch<SetStateAction<LoginUser | null>>; }; ... const [loginUser, setLoginUser] = useState<LoginUser | null>(null); この部分。 ここで、Firebase でログインしたりした時の戻り値の型が必要になるが、API ドキュメントには、UserCredential が返ってくると約束(可愛い)。 Provider に渡すのは、UserCredential.user だが、型はここを見てもよく分からない。 ので、こんな感じにした。 LoginUserProvider.tsx import { UserCredential } from "firebase/auth"; type LoginUser = UserCredential["user"]; 結局、これで良いのか良く分からないが、typescript が怒らないから(怒られた末こうなったから)、良いのかな? なお、auth インスタンスは、useAuth.ts の中で initialze した後に作っているが、これを export して使い回す必要はなく、必要な時に、getAuth() すれば良い。 おしまい セキュアで便利な Auth Provider を自前で作るのは、ぶっちゃけ大変だ。途方に暮れるレベルで大変だ。NextAuth.js 以上のことはやりたくない。Firebase 落ちたらどうすんの?みたいな話は別途考える必要があるとして、Firebase や Auth0 のようなサービスがあるのはとても助かる。要件次第ではあるものの、使える時は使っていきたい。 公式ドキュメントが非常にしっかりしていたので、思ったより、Firebase の実装は簡単だった。今回の実装時間も Firebase 部分は全体の1割くらい。結局、認証機能の実装は、それ以外の部分が複雑で混ぜるとより分かりづらくなるので、関心事は分離して考えるのが良い。 今回のソースコードは、Firebase の設定などを済ませると、ログイン後にうちの可愛いネコが拝める特典付き。おわり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React入門 - Part4.3 - 親子間コンポーネントへの値の受け渡し - Redux編

目次 なぜReduxなのか Part4.1や、Part4.2でProps、useContextを学びました。 いずれも表示値や制御値をどこで管理するかということになります。 propsの欠点は、バケツリレー。 バケツリレーをもう少し説明すると、 親、子、孫、ひ孫・・・・というコンポーネント階層があり 親で状態管理する(変数を表示する)要素があり、ひ孫でそれを更新する処理がある。逆も然り。 このような状況化で、propsで状態を受け渡すのは煩雑であり、また更新するたびに連なっているコンポーネントがレンダリングされることになりパフォーマンス的にも良くない。 (値を受け渡すだけで使用していないコンポーネントでもレンダリングが走るようです。またその対策もあるようですが、propsの中身がオブジェクトの場合はその対策も効かないようです。) 以下のようなイメージです。※こちらからイメージ画像いただきました。 propsの場合、孫①で更新された内容を子①、親、子②を通じて孫③へ伝達します。 Reduxの場合、孫①はStore上の値を更新し、孫③はStore上の値を読み取ります。 Reduxは、バケツリレーせずにグローバル値のように値を管理できるということになります。 よく状態管理という言葉が使われますが、私はなんだか馴染めない。。 (教えてください。。) Redux、Redux Toolkitについてちょっとだけ タイトルにRedux編と書いてますが、 Reduxは、使い易い感じはするが、ソースが多いので導入時のハードルが高い Redux ToolkitはReduxよりも導入が容易 2.の為、Reduxより主流になるのではという人もいます。 ということで、Redux Toolkitを導入してみることにしました。 ReduxではStoreという概念で状態管理を行いますが、Redux Toolkitでは、Slice という概念があるようです。(Sliceという概念の中にStoreを管理してる?) Reduxは家族を纏めて管理しているが、Redux Toolkitはお父さん、お母さん、長男などを別々に管理している感じとどこかに書いてありました。 ※どこかは忘れました。 「State + Actions + Reducer」のセットがsliceとのことです。 Actions、Reducerなどはググってみてください(難しく説明できません。。。雰囲気くらいつかんでおけばいいかと。。。) React + Typescript環境を作成 以下コマンドを実行します。 npx create-react-app プロジェクト名 --template redux-typescript 以下コマンドを実行し初期画面が表示されることを確認します。 yarn start 初期画面です。 ※下記画面が表示されない場合、ブラウザでlocalhost:3000にアクセスしてみてください。 Redux Toolkitの下準備 - src/rootReducer.tsの作成 import { combineReducers } from '@reduxjs/toolkit' const rootReducer = combineReducers({}) export type RootState = ReturnType<typeof rootReducer> export default rootReducer combineReducersが空ですが、reducerをどんどん作成し、ここに追加していくイメージかと思います。 Redux Toolkitの下準備 - src/store.tsの作成 configureStoreに先ほどのrootReducerを設定しstoreとして使えるようにします。 import { configureStore } from '@reduxjs/toolkit' import rootReducer from './rootReducer'   const store = configureStore({     reducer: rootReducer })   export type AppDispatch = typeof store.dispatch   export default store Redux Toolkitの下準備 - src/index.tsxの編集 共通のストアを各コンポーネントで使えるようにするためにトップコンポーネントのindex.tsxを次のように編集します。 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import * as serviceWorker from './serviceWorker'; // add start import { Provider } from 'react-redux' import store from './store' // add end ReactDOM.render( <React.StrictMode> {/* mod start */} {/* <App /> */} <Provider store={store}> <App /> </Provider> {/* mod end */} </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ちょっと休憩 用語の意味をちょっとだけ考えてみましょう。 ◆reducer 〔+目的語+to+(代)名詞〕〈ものを〉(整理して)〔簡単な形に〕変える,まとめる.  ⇒「状態とアクションを受け取って、次の状態を返すやつ」 ◆action アクションのタイプ(必須)とstate(値)のみ持つプレーンなオブジェクト 以下のようなイメージになります。 const action = { type: 'ADD_TODO', text: 'Go to swimming pool' }; storeによって、actionとstateがreducerに渡され、reducerで処理が行われます。 ◆store ReducerとStateにアクセスできるオブジェクト ◆概念 reduxを調べるとこんな図がよく出てきます。 ふむふむよくわかる。。って分かりませんよね。 最初はよく分かんなくていいと思います。(勝手。) 私も概念はまだよく理解していませんが、 reduxを利用するには、 ActionとかStoreを決まったように書いて(あるものをコピペして名前変えて) って使ってます。 でも最初にredux使える環境を作る時には困りますね。。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker×webpack×react×typescriptの環境構築(create-react-app不使用)

はじめに この記事ではcreate-react-appに頼ることなくreactの環境を作成する。dockerで構築しているが、手元にnode環境があれば飛ばしても良い。 完成後の構成 環境構築が終わると以下のようなフォルダ構成になる。 . ├── dist │   ├── index.html │   └── index.js ├── docker │   └── node │   └── Dockerfile ├── node_modules ├── src │   ├── main.tsx │   └── sample-component.tsx └── docker-compose.yml └── package-lock.json └── package.json └── tsconfig.json └── webpack.config.js dockerの環境設定 dockerではnodeの環境を構築する。 nodeのDokcefileは以下の通り Dockerfile FROM node:16-alpine WORKDIR /var/www/app WORKDIRは適当に設定した。nodeのバージョンは16-alpineとしたが、ここから自由に選んで良い(動作確認は16-alpineでしか行ってない)。 docker-compose.ymlは以下の通り docker-compose.yml version: "3.8" services: node: build: ./docker/node tty: true volumes: - ./:/var/www/app working_dir: /var/www/app ports: - 3000:3000 Dokcerfileとdocker-compose.ymlに分けたが、まとめても問題ない。portsなど細かい設定は任意に変えて良い。これらを設定したら、docker-compose up --build -dでDocker立ち上げると、Dockerのnode環境は完成。 Reactの環境設定 Reactの環境設定はdocker-compose exec node shでnodeのDokcerに入って全て行う(今後行うコマンド全て)。dockerを使わない場合はシェルにそのまま打てば良い。 プロジェクトの初期化 npmプロジェクトの立ち上げとして npm init -y と入力する。そうすると package.json { "name": "app", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } のようなファイルが生成される。立ち上げの際に詳細を設定したい場合は代わりにnpm initと入力して設定する。 必要なパッケージのインストール 今回必要最低限のパッケージとして以下のものをインストールする。 webpack: JSモジュールのバンドラー webpack-cli: webpackのCLI typescript ts-loader: typescriptをwebpackで処理するのに必要 react react-dom: DOMを管理するもの @types/react: reactの型定義 @types/react-dom: react-domの型定義 まず、reactとreact-dom以外は開発専用のパッケージなので npm i -D webpack webpack-cli typescript ts-loader @types/react @types/react-dom のようにインストールする。reactとreact-domは普通に npm i react react-dom のようにインストールする(nodeのバージョンが低ければオプションに-Sが必要)。今後必要なライブラリがあれば上記のようにインストールするとよい。 このようにインストールすることでpackage.jsonは package.json { "name": "app", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@types/react": "^17.0.20", "@types/react-dom": "^17.0.9", "ts-loader": "^9.2.5", "typescript": "^4.4.2", "webpack": "^5.52.0", "webpack-cli": "^4.8.0" }, "dependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" } } のように変更される。その他にもpackage-lock.jsonやnode_modulesが生成される。 typescriptの設定 typescriptの設定はtsconfig.jsonに書く。 tsconfig.json(最小限) { "compilerOptions": { "sourceMap": true, "target": "ES6", "module": "ES2020", "jsx": "react", "moduleResolution": "node", "lib": [ "ES2020", "DOM" ] } } tsconfigの設定は公式が見やすいのでこれを見て変更するとよい。何も考えずに動かす分には上記のものを使用すると良い。 webpackの構成 webpack.config.js const path = require("path"); module.exports = { mode: "development", entry: "./src/main.tsx", output: { filename: "main.js", path: path.resolve("dist"), publicPath: "/", }, module: { rules: [ { test: /\.tsx?$/, use: "ts-loader" } ] }, resolve: { extensions: [".ts", ".tsx", ".js", ".json"] }, }; この記事ではts,tsxファイルをsrcに置き、その中でもメインファイルをmain.tsxとしているのでentryは"./src/main.tsx"となった。また、出力先をdist、コンパイル後のファイル名をindex.jsとしたので、outputは { filename: "main.js", path: path.resolve("dist"), publicPath: "/", }, となった。moduleではts-loaderを使用する条件が書かれており、ts,tsxファイルに対して使用される。resolveはimport文の名前解決のために記述した。 typescriptのビルドコマンド追加 実際に上記で設定通りにts,tsxファイルをコンパイルするコマンドをpackage.jsonのscriptに追加する。 package.json "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "webpack", "watch": "webpack -w" }, このように追加することでnpm run buildでビルド、npm run watchでホットリロードが可能となる。その他scriptを追加するときはここに追加することとなる。これらのコマンドによってsrc内のmain.tsxがdistにindex.jsとしてコンパイルされる。 実際に動かしてみる テンプレートとして静的ファイルであるindex.htmlをdistに作成する。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <script defer src="./main.js"></script> </head> <body> <div id="app"></div> </body> </html> scriptでコンパイル後のjsファイルを読み込んでいる。<div id="app"></div>を埋め込む場所にReactが反映される(appでなくても良い)。 次にsrcにtypescriptで書かれたreactのファイルを配置する。サンプルとして簡単なファイル用意した。webpack.config.jsのresolveがうまく動いていることを確認するためにtsxのファイルを二個作成した(example-component.tsxがうまくimportされるかを確かめる)。 main.tsx import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { SampleComponent } from './sample-component'; class App extends React.Component { render() { return ( <div> <h1>React Example</h1> <SampleComponent name="Counter"/> </div> ); } } ReactDOM.render(<App/>, document.querySelector('#app')); 最終行のappがindex.htmlのdivのid(app)に対応している。 sample-component.tsx import * as React from 'react'; interface IProps { name: string; } export const SampleComponent = (props: IProps): React.ReactElement => { const [count, setCount] = React.useState<number>(0); return ( <div> <h1>{props.name}</h1> <div>{count}</div> <button onClick={() => setCount(count + 1)}>Count Up</button> </div> ); } 上記の三つのファイルを作成したのちにnpm run devを行い、./dist/index.jsにファイルが生成されたのを確認したら、index.htmlを開くと こんな感じの簡単なカウンターが現れる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】React ことはじめ (ただの学習ログ) Part2

きっかけ 前回の以下記事の続き 学習の進め方 前回の本で触れられていなかったAPI呼び出しやルーティングの内容が触れられているReactハンズオンラーニング第2版で学習したいところだけメモしています。 前回と同じく、記事の記載に間違いがある場合、マサカリをお願いします… 学習内容 ステート管理 アプリケーション全体のステート管理 複数コンポーネントがステートを持つと複雑になるため、1箇所でステートを管理したい→最上位のルートコンポーネントで管理、参照は親から子コンポーネントへプロパティとして渡す、変更は子から親コンポーネントへ伝えステートを更新する コンテキスト 巨大アプリケーションでのステート伝達の際に、中間コンポーネントが伝達のためだけにプロパティ設定が必要となり、複雑なアプリケーションとなってしまう。→コンテキストを使用することで解決。コンテキストプロバイダへデータを渡し、コンテキストコンシューマから読み出す データ API呼び出し fetch APIでHTTPリクエストを送信可能。GET, POST, PUTの場合はfetch APIのオプションとしてmethod内で指定。(axiosで実現する方法は別途確認) WebStorageへの保存 saveJSONでlocalStorageへの保存、loadJSONでlocalStorageからの読み出す。 ルーティング React Routerでルーティングを設定 遭遇したエラーとその対処 Module not found: You attempted to import XXX which falls outside of the project src/ directory. Relative imports outside of src/ are not supported. srcより上階層の資源を読み込もうとしたら発生。src配下に資源を移動、importのパスを修正 Syntax error: Unterminated JSX contents returnで書いていたJSXのタグの閉じ忘れが原因。 Manifest: Line: 1, column: 1, Syntax error Manifest.json create-react-app後にpublicの内容をindex.jsを除き、削除していたことが原因。manifest.jsonを復活。 ESLintで’jsx-a11y/href-no-hash’ was not found .eslintrc.json内でhref-no-hashをoffにする設定を追加。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React TypeScriptで動画を埋め込むなら読むべき記事

やりたいこと React TypeScript で開発していて、埋め込み動画を用意して、自動再生させたい! やった事の顛末をストーリー風に書いてみました。 あらすじ 動画読込 自動再生 リプレイ機能 Video系のライブラリっている? Reactで開発していて動画を再生したい!というとき、探すとそれっぽいライブラリが出てきます。これってライブラリ使わなきゃいけない理由でもあるの?と思ったので、無しでやったらどうなるのか試してみました。 結論から言うと、ブラウザの user agent stylesheet のおかげで割とリッチに仕上がるので、用途によってはライブラリ不要ですね。また、今回の手法は背景動画を仕込むときにも使えそうな方法です。ここからlazyloadとかの技術を足していくと良さそうです。 コード いきなりサブコンポーネントですが、こんな感じになりました。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> </React.StrictMode> ); } 動画をimportしてみる sample.tsx import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" で動画をソースに含めます。 TS(2307) このとき、mp4、 webmのimport部分でts(2307)のエラーが出ます。 モジュール ../movie/sample.mp4 またはそれに対応する型宣言が見つかりません。ts(2307) そこで、react-app-env.d.tsにdeclare文を追記します。 ./src/react-app-env.d.ts /// <reference types="react-scripts" /> declare module '*.mp4' { const src: string; export default src; } declare module '*.webm' { const src: string; export default src; } videoタグを設置 <video>を定義して、先ほどの動画を表示させます。 sample.tsx <video controls muted ref={videoRef} > controls を書くと、user agent stylesheet の再生ボタンとかを設定してくれます。 背景動画とかを置くなら controlsは消せばOKです。 refには、useRef()で参照を設定しています。これは後で解説します。 sample.tsx <source src={video_mp4} type="video/mp4" /> ここでは、先ほどソースに含めた動画をvideoの中で代替動画一覧に指定しています。ユーザー画面では、sourceのいずれかが実際に使用されます。 自動再生 しかし、sourceに動画を指定しただけでは再生が行われません。<video>にautoPlay属性を指定すると、自動再生されます。しかし今回は使用せず、代わりに、先ほどuseRef()で参照を設定して置いたものを活用しましょう。 sample.tsx useEffect(() => { videoRef.current?.play(); }, []); useEffect()でレンダリング終了後に、HtmlVideoElementのplay()をトリガーします。 追加要望でリプレイボタンを付けたい! 「まさか動画があるなんて!最初から見せてよ!」 「再生コントロールでスライダーをマウスでドラッグしろ?面倒なこと言わないで!」 という要望にお応えして、リプレイボタンを設置することになりました。 ボタンをクリックしたら、動画の最初に戻るような機能を加えてみましょう。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" + import { Button } from '@material-ui/core'; export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); + const StartReplay = () => { + videoRef.current?.currentTime = 0; + } return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> + <Button onClick={StartReplay} children="replay" variant="contained" color="primary" /> </React.StrictMode> ); } currentTimeで振り出しに コードの説明を見ていきましょう。 HtmlVideoElementのプロパティcurrentTimeをゼロにすると、動画の最初に戻ることは分かっています。そこで、StartReplay関数を用意します。その中で、currentTimeを操作する処理を入れます。 sample.tsx const StartReplay = () => { videoRef.current?.currentTime = 0; } TS(2779) さて、ここで問題発生です。currentTimeのところがエラーになります。 代入式の左辺には、省略可能なプロパティ アクセスを指定できません。ts(2779) 解決方法は、「存在する」と確実に分かる状況を作り出すことです。 sample.tsx const StartReplay = () => { if (videoRef.current?.currentTime) { videoRef.current.currentTime = 0; } } これでエラーは解決です。 始めに戻っただけ これで良し!と思いますが、残念。これだと、動画が終了後にリプレイボタンは機能しません。動画内のどの時刻にカーソルがあるかを示すcurrentTimeと、再生と停止を司るplay()は別の問題なのです。 sample.tsx const StartReplay = () => { if (videoRef.current?.currentTime) { videoRef.current.currentTime = 0; } videoRef.current?.play(); } currentTimeをゼロにした後に、play()で再生を開始させましょう。 まとめ 最後にまとめてみましょう。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" + import { Button } from '@material-ui/core'; export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); + const StartReplay = () => { + if (videoRef.current?.currentTime) { + videoRef.current.currentTime = 0; + } + videoRef.current?.play(); + } return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> + <Button onClick={StartReplay} children="replay" variant="contained" color="primary" /> </React.StrictMode> ); } 環境 create-react-app : 4.0.3 react : 17 typescript : 4 ./tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] } 関連公式 Excelsior!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React TypeScriptでvideoタグだけで動画を読み込む

やりたいこと React TypeScript で開発していて、埋め込み動画を用意して、自動再生させたい! やった事の顛末をストーリー風に書いてみました。 あらすじ 動画読込 自動再生 リプレイ機能 Video系のライブラリっている? Reactで開発していて動画を再生したい!というとき、探すとそれっぽいライブラリが出てきます。これってライブラリ使わなきゃいけない理由でもあるの?と思ったので、無しでやったらどうなるのか試してみました。 結論から言うと、ブラウザの user agent stylesheet のおかげで割とリッチに仕上がるので、用途によってはライブラリ不要ですね。また、今回の手法は背景動画を仕込むときにも使えそうな方法です。ここからlazyloadとかの技術を足していくと良さそうです。 コード いきなりサブコンポーネントですが、こんな感じになりました。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> </React.StrictMode> ); } 動画をimportしてみる sample.tsx import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" で動画をソースに含めます。 TS(2307) このとき、mp4、 webmのimport部分でts(2307)のエラーが出ます。 モジュール ../movie/sample.mp4 またはそれに対応する型宣言が見つかりません。ts(2307) そこで、react-app-env.d.tsにdeclare文を追記します。 ./src/react-app-env.d.ts /// <reference types="react-scripts" /> declare module '*.mp4' { const src: string; export default src; } declare module '*.webm' { const src: string; export default src; } videoタグを設置 <video>を定義して、先ほどの動画を表示させます。 sample.tsx <video controls muted ref={videoRef} > controls を書くと、user agent stylesheet の再生ボタンとかを設定してくれます。 背景動画とかを置くなら controlsは消せばOKです。 refには、useRef()で参照を設定しています。これは後で解説します。 sample.tsx <source src={video_mp4} type="video/mp4" /> ここでは、先ほどソースに含めた動画をvideoの中で代替動画一覧に指定しています。ユーザー画面では、sourceのいずれかが実際に使用されます。 自動再生 しかし、sourceに動画を指定しただけでは再生が行われません。<video>にautoPlay属性を指定すると、自動再生されます。しかし今回は使用せず、代わりに、先ほどuseRef()で参照を設定して置いたものを活用しましょう。 sample.tsx useEffect(() => { videoRef.current?.play(); }, []); useEffect()でレンダリング終了後に、HtmlVideoElementのplay()をトリガーします。 追加要望でリプレイボタンを付けたい! 「まさか動画があるなんて!最初から見せてよ!」 「再生コントロールでスライダーをマウスでドラッグしろ?面倒なこと言わないで!」 という要望にお応えして、リプレイボタンを設置することになりました。 ボタンをクリックしたら、動画の最初に戻るような機能を加えてみましょう。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" + import { Button } from '@material-ui/core'; export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); + const StartReplay = () => { + videoRef.current?.currentTime = 0; + } return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> + <Button onClick={StartReplay} children="replay" variant="contained" color="primary" /> </React.StrictMode> ); } currentTimeで振り出しに コードの説明を見ていきましょう。 HtmlVideoElementのプロパティcurrentTimeをゼロにすると、動画の最初に戻ることは分かっています。そこで、StartReplay関数を用意します。その中で、currentTimeを操作する処理を入れます。 sample.tsx const StartReplay = () => { videoRef.current?.currentTime = 0; } TS(2779) さて、ここで問題発生です。currentTimeのところがエラーになります。 代入式の左辺には、省略可能なプロパティ アクセスを指定できません。ts(2779) 解決方法は、「存在する」と確実に分かる状況を作り出すことです。 sample.tsx const StartReplay = () => { if (videoRef.current?.currentTime) { videoRef.current.currentTime = 0; } } これでエラーは解決です。 始めに戻っただけ これで良し!と思いますが、残念。これだと、動画が終了後にリプレイボタンは機能しません。動画内のどの時刻にカーソルがあるかを示すcurrentTimeと、再生と停止を司るplay()は別の問題なのです。 sample.tsx const StartReplay = () => { if (videoRef.current?.currentTime) { videoRef.current.currentTime = 0; } videoRef.current?.play(); } currentTimeをゼロにした後に、play()で再生を開始させましょう。 まとめ 最後にまとめてみましょう。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" + import { Button } from '@material-ui/core'; export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); + const StartReplay = () => { + if (videoRef.current?.currentTime) { + videoRef.current.currentTime = 0; + } + videoRef.current?.play(); + } return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> + <Button onClick={StartReplay} children="replay" variant="contained" color="primary" /> </React.StrictMode> ); } 環境 create-react-app : 4.0.3 react : 17 typescript : 4 ./tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] } 関連公式 Excelsior!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React TypeScriptで動画を埋め込むときにハマったところ

やりたいこと React TypeScript で開発していて、埋め込み動画を用意して、自動再生させたい! やった事の顛末をストーリー風に書いてみました。 あらすじ 動画読込 自動再生 リプレイ機能 Video系のライブラリっている? Reactで開発していて動画を再生したい!というとき、探すとそれっぽいライブラリが出てきます。これってライブラリ使わなきゃいけない理由でもあるの?と思ったので、無しでやったらどうなるのか試してみました。 結論から言うと、ブラウザの user agent stylesheet のおかげで割とリッチに仕上がるので、用途によってはライブラリ不要ですね。また、今回の手法は背景動画を仕込むときにも使えそうな方法です。ここからlazyloadとかの技術を足していくと良さそうです。 コード いきなりサブコンポーネントですが、こんな感じになりました。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> </React.StrictMode> ); } 動画をimportしてみる sample.tsx import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" で動画をソースに含めます。 TS(2307) このとき、mp4、 webmのimport部分でts(2307)のエラーが出ます。 モジュール ../movie/sample.mp4 またはそれに対応する型宣言が見つかりません。ts(2307) そこで、react-app-env.d.tsにdeclare文を追記します。 ./src/react-app-env.d.ts /// <reference types="react-scripts" /> declare module '*.mp4' { const src: string; export default src; } declare module '*.webm' { const src: string; export default src; } videoタグを設置 <video>を定義して、先ほどの動画を表示させます。 sample.tsx <video controls muted ref={videoRef} > controls を書くと、user agent stylesheet の再生ボタンとかを設定してくれます。 背景動画とかを置くなら controlsは消せばOKです。 refには、useRef()で参照を設定しています。これは後で解説します。 sample.tsx <source src={video_mp4} type="video/mp4" /> ここでは、先ほどソースに含めた動画をvideoの中で代替動画一覧に指定しています。ユーザー画面では、sourceのいずれかが実際に使用されます。 自動再生 しかし、sourceに動画を指定しただけでは再生が行われません。<video>にautoPlay属性を指定すると、自動再生されます。しかし今回は使用せず、代わりに、先ほどuseRef()で参照を設定して置いたものを活用しましょう。 sample.tsx useEffect(() => { videoRef.current?.play(); }, []); useEffect()でレンダリング終了後に、HtmlVideoElementのplay()をトリガーします。 追加要望でリプレイボタンを付けたい! 「まさか動画があるなんて!最初から見せてよ!」 「再生コントロールでスライダーをマウスでドラッグしろ?面倒なこと言わないで!」 という要望にお応えして、リプレイボタンを設置することになりました。 ボタンをクリックしたら、動画の最初に戻るような機能を加えてみましょう。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" + import { Button } from '@material-ui/core'; export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); + const StartReplay = () => { + videoRef.current?.currentTime = 0; + } return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> + <Button onClick={StartReplay} children="replay" variant="contained" color="primary" /> </React.StrictMode> ); } currentTimeで振り出しに コードの説明を見ていきましょう。 HtmlVideoElementのプロパティcurrentTimeをゼロにすると、動画の最初に戻ることは分かっています。そこで、StartReplay関数を用意します。その中で、currentTimeを操作する処理を入れます。 sample.tsx const StartReplay = () => { videoRef.current?.currentTime = 0; } TS(2779) さて、ここで問題発生です。currentTimeのところがエラーになります。 代入式の左辺には、省略可能なプロパティ アクセスを指定できません。ts(2779) 解決方法は、「存在する」と確実に分かる状況を作り出すことです。 sample.tsx const StartReplay = () => { if (videoRef.current?.currentTime) { videoRef.current.currentTime = 0; } } これでエラーは解決です。 始めに戻っただけ これで良し!と思いますが、残念。これだと、動画が終了後にリプレイボタンは機能しません。動画内のどの時刻にカーソルがあるかを示すcurrentTimeと、再生と停止を司るplay()は別の問題なのです。 sample.tsx const StartReplay = () => { if (videoRef.current?.currentTime) { videoRef.current.currentTime = 0; } videoRef.current?.play(); } currentTimeをゼロにした後に、play()で再生を開始させましょう。 まとめ 最後にまとめてみましょう。 sample.tsx import React, { useRef, useEffect } from 'react' import video_mp4 from "../movie/sample.mp4" import video_webm from "../movie/sample.webm" + import { Button } from '@material-ui/core'; export function Translate() { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { videoRef.current?.play(); }, []); + const StartReplay = () => { + if (videoRef.current?.currentTime) { + videoRef.current.currentTime = 0; + } + videoRef.current?.play(); + } return ( <React.StrictMode> <video controls muted ref={videoRef} > <source src={video_mp4} type="video/mp4" /> <source src={video_webm} type="video/webm" /> <p>Your browser doesn't support HTML5 video.</p> </video> + <Button onClick={StartReplay} children="replay" variant="contained" color="primary" /> </React.StrictMode> ); } 環境 create-react-app : 4.0.3 react : 17 typescript : 4 ./tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] } 関連公式 Excelsior!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】React AppをGitHub Pagesでデプロイする(2021年9月)

はじめに Reactで作成したWebアプリをGitHub Pagesでデプロイする際に結構躓いたので,こうすると出来たよという現時点での情報を備忘録として残す。 環境 Windows 完成品 https://fk-ln.github.io/react-test/ とりあえずデプロイの手順だけを示すので,使うリポジトリはnpx create-react-appで作成したデフォルトのサンプル。 手順 GitHubでリポジトリを作成してmain(master)リポジトリにpush ローカルリポジトリでnpm install gh-pages --save-dev package.jsonを開いて以下を追記(/と/Scripts/の二か所) { ~省略~ "homepage": "https://fk-ln.github.io/tdu-new-school/",  ※GitHubの`Settings`→`Pages`に表示されるURLをコピペ ~省略~ "Scripts": { ~省略~ deploy": "npm run build && gh-pages -d build", } ~省略~ } npm run buildを実行 GitHubのSettings→PagesでBranchをgh-pages,フォルダを/(root)に設定 これで先ほど設定した(package.jsonに書いた)URLにアクセスするとアプリが使えるはず。(反映に数分かかるかも) gh-pages gh-pagesというnpmパッケージではソースコードをコンパイルしてbuildフォルダを作成し,それをgh-pagesブランチにpushしてくれる。 躓いた点 package.jsonに"homepage":を書いていなかった  →どうやらデプロイ先のURLは明示的に示す必要があるっぽい 操作にWindows Powershellを使っていた  →npm run buildではGitHubにpushする作業が行われるのでgit bashなどのgitコマンドを使える環境で行う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

styled-componentsでカード componentを作ってみる

作ったcomponentは以下。 イメージがトップに置かれていて、その下にlast updated dateがあり、その下にカードのタイトルとカードに関する記述。そして、一番下にリンクを2つ。 もともとは自分のポートフォリオサイトにあるプロジェクト用にカードのコンポーネントを試していたので、リンクを2つ設置しています。 あとは必要ないけれど、tilt animationをreact-parallax-tiltというライブラリで加えています。 フォルダ構成はシンプルにcomponents/Card/index.tsxを作って、style-componentsは別ファイルにして、importして使っています。 propsとしてカードのtitle, date, imageをCard componentに渡すようにしました。 Card/index.tsx import { CardType } from "../../Types"; import { CardWrapper, CardImage, CardTextWrapper, CardTextDate, CardTextTitle, CardTextBody, CardStatWrapper, CardStats, LinkText } from "./CardStyles"; import Tilt from "react-parallax-tilt"; export const Card = ({ title, date, imgUrl }: CardType) => { return ( <Tilt> <CardWrapper> <CardImage background={imgUrl} /> <CardTextWrapper> <CardTextDate>{date} days ago</CardTextDate> <CardTextTitle>{title}</CardTextTitle> <CardTextBody> Lorem ipsum dolor sit amet consectetur, Ducimus, repudiandae temporibus omnis illum maxime quod deserunt eligendi dolor </CardTextBody> </CardTextWrapper> <CardStatWrapper> {/* <CardStats> <div> 1<sup>m</sup> </div> <div>read</div> </CardStats> */} <CardStats> <LinkText href="#">website</LinkText> </CardStats> <CardStats> <LinkText href="#">github</LinkText> </CardStats> </CardStatWrapper> </CardWrapper> </Tilt> ); }; Card/CardStyles.ts import styled from "styled-components"; export const CardWrapper = styled.div` display: grid; grid-template-columns: 300px; grid-template-rows: 210px 210px 80px; grid-template-areas: "image" "text" "stats"; border-radius: 18px; background: #000; box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.9); text-align: center; `; export const CardImage = styled.div<{ background: string }>` grid-area: image; background-image: url(${({ background }) => background}); border-top-left-radius: 15px; border-top-right-radius: 15px; background-size: cover; `; export const CardTextWrapper = styled.div` grid-area: text; margin: 25px; `; export const CardTextDate = styled.span` color: rgb(255, 7, 110); font-size: 13px; `; export const CardTextTitle = styled.h2` margin-top: 0px; font-size: 2rem; box-sizing: border-box; min-width: 0px; line-height: 1.2; margin: 0px; background: linear-gradient( 110.78deg, rgb(118, 230, 80) -1.13%, rgb(249, 214, 73) 15.22%, rgb(240, 142, 53) 32.09%, rgb(236, 81, 87) 48.96%, rgb(255, 24, 189) 67.94%, rgb(26, 75, 255) 85.34%, rgb(98, 216, 249) 99.57% ); background-clip: text; -webkit-background-clip: text; color: transparent; `; export const CardTextBody = styled.p` color: grey; font-size: 15px; font-weight: 300; `; export const CardStatWrapper = styled.div` grid-area: stats; display: grid; /* grid-template-columns: 1fr 1fr 1fr; */ grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; background: #5930e5; `; export const CardStats = styled.div` display: flex; align-items: center; justify-content: center; flex-direction: column; color: white; padding: 10px; `; export const LinkText = styled.a` color: #fff; text-decoration: none; `; codesandbox
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react アプリ開発環境を準備する (TypeScript, commitlint, husky, Sourcetree)

概要 react アプリを GitLab Pages 上に公開するまでの環境設定を、ローカルでの開発環境と GitLab CI/CD の設定に分けてまとめておく。 ローカルでの開発環境 この部分は GitLab CI/CD とは密には関係していないが、 commit log を semantic versioning に準拠した形で記述することを維持するために細工をしておく。 この環境設定を行なった git repo はこちら。https://gitlab.com/ken.miyasita/react-app-on-gitlab-pages/-/tree/main 全体像 react アプリケーションの雛形を準備 参照: Create React App $ node --version v15.14.0 $ npx create-react-app react-app-on-gitlab-pages --template typescript $ cd react-app-on-gitlab-pages $ yarn start eslint 参照 * Getting Started with ESLint - ESLint - Pluggable JavaScript linter * Getting Started - Linting your TypeScript Codebase * eslint-plugin-simple-import-sort lint ルールをいくつか設定しておく。 文字列は、全てシングルクオートマークにする import 文は、いくつかのカテゴリに分け、それぞれのカテゴリー内は import 対象のパス名でソートする $ yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-simple-import-sort --dev .eslintrc.json { "env": { "browser": true, "es6": true, "node": true }, "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint", "simple-import-sort" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended" ], "rules": { "quotes": [ "error", "single" ], "simple-import-sort/imports": "error", "simple-import-sort/exports": "error" } } package.json "scripts": { "lint": "eslint . --ext .ts,.tsx" }, jest 参照:Running Tests | Create React App Create react app が自動生成する package.json の script 定義は以下のようになっており、 環境変数 CI が設定されているか否かで test の挙動が変化するようになっている。 package.json "scripts": { "test": "react-scripts test", }, CI が定義されていないときには watch モードで動作。ソースコードが変更されるたびに、変更されたソースコードに関連するテストのみ実行される。 CI が定義されているときには、全テストを実行。Predefined variables reference - GitLab によると GitLab CI/CD 環境でも CI=true が設定されている。 もしくは yarn test --watchAll=false として起動すれば、全テストを実行。この機能は後で commit 時に test を実行するために利用する。 commitlint を husky を使って起動する 参照: * commitlint - Lint commit messages * commitlint - Lint commit messages > Guide: Local setup commit log を semantic versioning に準拠した形に維持するために commitlint を導入する。 $ yarn add @commitlint/{cli,config-conventional} --dev $ echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js $ yarn add husky --dev $ yarn husky install $ yarn husky add .husky/commit-msg 'yarn commitlint --edit $1' semantic versioning に適合しない commit log を指定して commit しようとしてもエラーとなることが確認できる。 $ git commit -m'foo:aaa' yarn run v1.22.11 $ /Users/kmiyashita/gitlab/react-app-on-gitlab-pages/node_modules/.bin/commitlint --edit .git/COMMIT_EDITMSG ⧗ input: foo:aaa ✖ subject may not be empty [subject-empty] ✖ type may not be empty [type-empty] ✖ found 2 problems, 0 warnings ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. husky - commit-msg hook exited with code 1 (error) commit 時に lint, jest を起動する せっかく husky を導入したので、commit 時には lint, jest が必ずパスすることを確認することにする。 $ yarn husky add .husky/pre-commit 'yarn lint && yarn test --watchAll=false' 確かに、lint と jest が実行されることを確認できた。 $ git commit -m 'feat: run lint and jest at every commit' yarn run v1.22.11 $ eslint . --ext .ts,.tsx ✨ Done in 0.58s. yarn run v1.22.11 $ react-scripts test --watchAll=false PASS src/App.test.tsx ✓ renders learn react link (14 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.006 s Ran all test suites. ✨ Done in 1.56s. yarn run v1.22.11 $ /Users/kmiyashita/gitlab/react-app-on-gitlab-pages/node_modules/.bin/commitlint --edit .git/COMMIT_EDITMSG ✨ Done in 0.21s. [main 0a560aa] feat: run lint and jest at every commit 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100755 .husky/pre-commit ただ、開発中の feature branch では lint, jest が一時的にパスしない状態でも commit をして状態保存をしながら開発し、最後に interactive rebase で commit log を整理してから main branch にマージすることも多々あると予想される。それを考えると常に lint, jest がパスしないと commit できないというルールは厳しすぎるかもしれないので、結局以下のように main branch でのみこのルールを発動するようにした。 .husky/pre-commit #!/bin/sh . "$(dirname "$0")/_/husky.sh" branch="$(git rev-parse --abbrev-ref HEAD)" if [ "$branch" = "main" ]; then yarn lint && yarn test --watchAll=false fi Sourcetree から commit したときにも commit hook を動作させる 参考: * Launching a macOS application with a custom path * Can I change the application icon of an Automator script? ここまでで Terminal から git 操作した時の挙動はうまく設定できたが、Sourcetree から commit をしようとするとうまく動作しない。以下のように yarn が見つからないというエラーが出る。 これは、Sourcetree のように Dock / Finder から起動されたアプリにとっては PATH 変数がデフォルト設定のままになっており、自分で yarn 等をインストールしたフォルダーを .zshrc 等で PATH 変数に追加していてもそれが反映されないことが原因である。 この問題を回避するためには、Automator を利用して下図のように .zshrc 等を読み込んでから Sourcetree を起動すれば良い。ある意味、shell script を Dock / Finder に置いていると考えられる。 このようにして PATH 設定をした状態の Sourcetree を用いると、下図のように commit 時に lint, jest が起動される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

10分で理解: react アプリ開発環境を準備 (TypeScript, commitlint, husky, Sourcetree)

概要 react アプリを TypeScipt で開発する際の定番の設定をまとめておく。 TypeScript で記述 eslint で構文チェック(クォーテーションマークの統一、import 文のソート) jest でユニットテスト commitlint で commit log が semantic versioning に準拠しているかチェックし、エラーが出たときには commit 中止 commit 前に eslint, jest を自動実行し、エラーが出たときには commit 中止 Sourcetree からも上記のフックが正しく実行されるように細工 この環境設定を行なった git repo はこちら。https://gitlab.com/ken.miyasita/react-app-on-gitlab-pages/-/tree/main 全体像 react アプリケーションの雛形を準備 参照: Create React App $ node --version v15.14.0 $ npx create-react-app react-app-on-gitlab-pages --template typescript $ cd react-app-on-gitlab-pages $ yarn start eslint 参照 * Getting Started with ESLint - ESLint - Pluggable JavaScript linter * Getting Started - Linting your TypeScript Codebase * eslint-plugin-simple-import-sort lint ルールをいくつか設定しておく。 文字列は、全てシングルクオートマークにする import 文は、いくつかのカテゴリに分け、それぞれのカテゴリー内は import 対象のパス名でソートする $ yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-simple-import-sort --dev .eslintrc.json { "env": { "browser": true, "es6": true, "node": true }, "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint", "simple-import-sort" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended" ], "rules": { "quotes": [ "error", "single" ], "simple-import-sort/imports": "error", "simple-import-sort/exports": "error" } } package.json "scripts": { "lint": "eslint . --ext .ts,.tsx" }, jest 参照:Running Tests | Create React App Create react app が自動生成する package.json の script 定義は以下のようになっており、 環境変数 CI が設定されているか否かで test の挙動が変化するようになっている。 package.json "scripts": { "test": "react-scripts test", }, CI が定義されていないときには watch モードで動作。ソースコードが変更されるたびに、変更されたソースコードに関連するテストのみ実行される。 CI が定義されているときには、全テストを実行。Predefined variables reference - GitLab によると GitLab CI/CD 環境でも CI=true が設定されている。 もしくは yarn test --watchAll=false として起動すれば、全テストを実行。この機能は後で commit 時に test を実行するために利用する。 commitlint を husky を使って起動する 参照: * commitlint - Lint commit messages * commitlint - Lint commit messages > Guide: Local setup commit log を semantic versioning に準拠した形に維持するために commitlint を導入する。 $ yarn add @commitlint/{cli,config-conventional} --dev $ echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js $ yarn add husky --dev $ yarn husky install $ yarn husky add .husky/commit-msg 'yarn commitlint --edit $1' semantic versioning に適合しない commit log を指定して commit しようとしてもエラーとなることが確認できる。 $ git commit -m'foo:aaa' yarn run v1.22.11 $ /Users/kmiyashita/gitlab/react-app-on-gitlab-pages/node_modules/.bin/commitlint --edit .git/COMMIT_EDITMSG ⧗ input: foo:aaa ✖ subject may not be empty [subject-empty] ✖ type may not be empty [type-empty] ✖ found 2 problems, 0 warnings ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. husky - commit-msg hook exited with code 1 (error) commit 時に lint, jest を起動する せっかく husky を導入したので、commit 時には lint, jest が必ずパスすることを確認することにする。 $ yarn husky add .husky/pre-commit 'yarn lint && yarn test --watchAll=false' 確かに、lint と jest が実行されることを確認できた。 $ git commit -m 'feat: run lint and jest at every commit' yarn run v1.22.11 $ eslint . --ext .ts,.tsx ✨ Done in 0.58s. yarn run v1.22.11 $ react-scripts test --watchAll=false PASS src/App.test.tsx ✓ renders learn react link (14 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.006 s Ran all test suites. ✨ Done in 1.56s. yarn run v1.22.11 $ /Users/kmiyashita/gitlab/react-app-on-gitlab-pages/node_modules/.bin/commitlint --edit .git/COMMIT_EDITMSG ✨ Done in 0.21s. [main 0a560aa] feat: run lint and jest at every commit 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100755 .husky/pre-commit ただ、開発中の feature branch では lint, jest が一時的にパスしない状態でも commit をして状態保存をしながら開発し、最後に interactive rebase で commit log を整理してから main branch にマージすることも多々あると予想される。それを考えると常に lint, jest がパスしないと commit できないというルールは厳しすぎるかもしれないので、結局以下のように main branch でのみこのルールを発動するようにした。 .husky/pre-commit #!/bin/sh . "$(dirname "$0")/_/husky.sh" branch="$(git rev-parse --abbrev-ref HEAD)" if [ "$branch" = "main" ]; then yarn lint && yarn test --watchAll=false fi Sourcetree から commit したときにも commit hook を動作させる 参考: * Launching a macOS application with a custom path * Can I change the application icon of an Automator script? ここまでで Terminal から git 操作した時の挙動はうまく設定できたが、Sourcetree から commit をしようとするとうまく動作しない。以下のように yarn が見つからないというエラーが出る。 これは、Sourcetree のように Dock / Finder から起動されたアプリにとっては PATH 変数がデフォルト設定のままになっており、自分で yarn 等をインストールしたフォルダーを .zshrc 等で PATH 変数に追加していてもそれが反映されないことが原因である。 この問題を回避するためには、Automator を利用して下図のように .zshrc 等を読み込んでから Sourcetree を起動すれば良い。ある意味、shell script を Dock / Finder に置いていると考えられる。 このようにして PATH 設定をした状態の Sourcetree を用いると、下図のように commit 時に lint, jest が起動される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む