20211128のReactに関する記事は21件です。

【デプロイ】Next.jsをGoogle App Engine上にデプロイを行う

はじめに Next.js をGoogle App Engine 上にデプロイする方法をまとめます ① GCPをインストール $ brew install google-cloud-sdk $ which gcloud /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud source "/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.bash.inc" source "/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/completion.bash.inc" ※zshの場合は、path.bash.inc → path.zsh.inc、completion.bash.inc → completion.zsh.inc に変更する ② 認証情報などを設定する $ gcloud init 説明に従って設定していく ③ package.json 修正 ・ドキュメント アプリケーションの起動 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", + "start": "next start -p $PORT", + "deploy": "yarn build && gcloud app deploy", "lint": "next lint" }, "dependencies": { ④ App Engine アプリの設定ファイル、 app.yamlを作成する ・ドキュメント app.yaml 構成ファイル (例) runtime: nodejs15 handlers: - url: /.* secure: always script: auto ⑤ デプロイ $ npm run deploy か $ yarn deploy 以下のようになったらデプロイ完了 target url に書かれている urlにアクセスすると、画面表示されました GCPの環境のAPP Engine にアクセスして、サービスの中をみると存在しています。ここで削除ができます。 参考 Next.jsで作ったアプリをGoogle App Engineにデプロイする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで複数classNameを扱いづらかったので使いやすくしてみた

ReactのclassNameが使いづらい ReactでclassNameが複数あったり、条件によって付けたりつけなかったりするときに //複数 <div className={ "btn" + " " + "red" }> </div> //条件分岐 <div className={ condition ? "red" : "blue" }> </div> //組み合わせ <div className={ "btn" + " " +(condition ? "red" : "blue" ) }> </div> うん、見にくいし、" "をつけ忘れる。 一応classNamesなるライブラリがあるが、いまいち使いづらいので、自分で作ってしまいました。 結論 cn関数を実装しました cnの使い方 <div className={cn( "btn", [condition,"red"], [condition,"red","blue"], )}> </div> cn関数の実装 function cn(...classNames:(string|[boolean,string,string?])[]):string{ let ans = "" ; classNames.forEach((className)=>{ if(typeof className === "string" ){ ans += className ; ans += " " ; }else if(className instanceof Array){ const [con,t,f=""] = className ; if(con){ ans += t ; }else{ ans += f ; } ans += " " ; }else{ throw new Error("className must be string or array "+className) ; } }); return ans ; } 独り言 switch的な感じで cn( [type, 0,"type-0", 1,"type-1", 2,"type-2", ], ) って書けたらもっと幸せですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MUI (Material-UI) で簡単なクイズアプリを作った話

概要 React の学習を兼ねて,新しくなった MUI (Material-UI) を用いたクイズアプリを作成した.作成したクイズアプリは GitHub Pages で公開している.また,ソースコードは GitHub にある. 詳細 具体的な実装については,GitHub にあるソースコードを見ていただくことにし,本記事ではプログラムの構成や役割について述べる. プログラムの構成 GitHub リポジトリの src ディレクトリ内にあるプログラムのうち,クイズアプリを実現する本質的なものは以下の5つである. │ quiz.js │ └─components qbcheckboxlist.js qbradiolist.js qbtextfield.js quizboard.js 以下,この5つのプログラムの役割を説明する. プログラムの役割 quiz.js アプリに表示する問題を記述するプログラムである.以下にその一例を示す. quiz.js const quiz = [ { id: 1, statement: '10000000の100000倍は1(  )である。括弧にあてはまる漢字1字を答えよ。', type: 'text', answer: '兆', }, { id: 2, statement: '4と6の最小公倍数はいくつか。', type: 'number', answer: '12', }, ]; 上記のように,アプリに表示する問題をオブジェクトの配列 quiz として記述する.オブジェクトの各プロパティの意味は次の通りである. id : 問題番号 statement : 問題文 type : 回答欄の種類 answer : 正答 プロパティ type には text,number,radio,checkbox の4種類が用意されている.radio,checkbox を指定した場合,上記4つのプロパティに加えて choices : 選択肢 が必要である.choice には,選択肢をオブジェクトの配列で次のように指定する. choices: [ { value: 'choice1', label: '偶数と偶数の和は奇数である。', }, { value: 'choice2', label: '偶数と奇数の和は偶数である。', }, { value: 'choice3', label: '奇数と偶数の和は奇数である。', }, { value: 'choice4', label: '奇数と奇数の和は奇数である。', }, ] プロパティ value には,各選択肢を表す一意的なIDを指定する.value に指定された値は,前述のプロパティ answer で正答を指定するのに用いる.また,プロパティ label には,クイズアプリに表示される選択肢の文言を指定する. さらに,プロパティ type に checkbox を指定した場合,プロパティ answer には正答を配列で指定する.以下にその一例を示す. { id: 4, statement: '次のうち、素数であるものをすべて選べ。', type: 'checkbox', choices: [ { value: 'choice1', label: '1', }, { value: 'choice2', label: '2', }, { value: 'choice3', label: '3', }, { value: 'choice4', label: '4', }, ], answer: [ 'choice2', 'choice3', ], } quizboard.js quiz.js に記述された各オブジェクトを基に,クイズを画面に表示するコンポーネントである.quiz.js の記述に問題がある場合は,エラーを表示する. エラーが表示される例 プロパティ type に radio を指定しているが,プロパティ choice が配列で指定されていない qbtextfield.js quizboard.js の子コンポーネントであり,回答欄にテキストボックスを表示する.quiz.js において,プロパティ type に text,number が指定された場合に使用される. qbradiolist.js quizboard.js の子コンポーネントであり,回答欄にラジオボタンリストを表示する.quiz.js において,プロパティ type に radio が指定された場合に使用される. qbcheckboxlist.js quizboard.js の子コンポーネントであり,回答欄にチェックボックスリストを表示する.quiz.js において,プロパティ type に checkbox が指定された場合に使用される. 参考 MUI: The React component library you always wanted
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kubernetes(AWS EKS)+go gin+React+Three.js+websocketでVR YouTube ウォッチパーティー作ってみた

website: Ygg 機能面 現状の機能 Syncボタンで再生同期 アバター(という名の緑色の箱)をwasdキーで移動 簡易チャットでコメント共有 動画IDで再生する動画を選択 動画のミュート アンミュート これから実装したい機能 再生したい動画によって部屋分け アバターエモート スマートフォンのブラウザからも動かせるようにする モチベーションとアーキテクチャ選定 モチベーション kubernetesを少しだけ触る機会があり、これは継続して使用しないと何も身につかないと思い何かを作ってみようと決心 ...YoutubeのVRウォッチパーティーにするか! アーキテクチャ選定 インフラ: kubernetes on AWS EKS Dockerコンテナベースでアプリケーションのスケーリングができるということで学習意欲が湧く バックエンド: golang gin バックエンドを静的型付け言語で書いてみたい & 流行に合わせてgoで書いてみたい → go framework間で比較 → 人気と実装の簡便さからginを選択 フロントエンド: React React か Vueかで迷う → React vs Vue.js、2021年に勢力図を広げるのはどちらでしょうか? + いつかReact Nativeも学んでみたい VR 表現: Three.js クライアントGPUでレンダリング処理できるWebGLを簡単に扱えるJavaScriptライブラリ UnityをWebGLビルドするという選択肢も考えられたが、Unity未経験のため断念 フロントエンド - バックエンド 通信: Websocket TCPとUDPで迷ったが、TCP(Websocket)のほうが実装&プロトコル管理の観点で簡単に思えたということと、高リアルタイム性が求められているわけではないので一旦Websocketを選択 折角なので未経験だが興味のある分野もりもりで試してみることに システム構成図 k8s クラスターはeksctl create clusterで作成。 Ingress ControllerとしてAWS ALBを設定することで必要に応じてALBがプロトコルをhttps→wssにアップデートしてくれるらしく、Websocket通信を行う上では便利 Route53でドメインを取得し、ACMで取得した証明書をあてる 詳しくは はじめてのdraw.ioでAWS EKSシステム構成図作成 開発Tips 以降このページを目次として開発時の記事を追加していく予定です!超初歩的なことからわからなかったので超初歩的なことから書く予定です。 ↓執筆済み - はじめてのdraw.ioでAWS EKSシステム構成図作成 ↓ 雑に執筆予定 - Docker環境での開発(マルチステージビルドで軽量コンテナのススメ) - AWS 基本事項(EKSはどのようなAWSリソースで成り立っているのか?) - kubernetes 基本リソース(Node Pod Deployment Service Ingress) - Nodejs(create react appとは webpack,babelとは) - Go開発(Go playground, go.mod) - Three.js(Three.jsでDOMエレメントをレンダリングする方法) おわりに qiita初投稿&めちゃめちゃ初学者なので、間違っている部分多々あると思います。ご指摘待ってます。 参考 Golang フレームワーク比較 React vs Vue.js、2021年に勢力図を広げるのはどちらでしょうか?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FastAPI + Reactでフルスタックアプリを作成する

この記事は「【マイスター・ギルド】本物のAdvent Calendar 2021」1日目の記事です。 本記事ではFastAPI + Reactを使ったシンプルなフルスタックアプリの実装方法の紹介および所感を行っていきます。 想定読者 React + TypeScriptの基本的な書き方を理解されている方。 FastAPIに初めて触れる方。 React + FastAPIで実装してみたい方。 今回のゴール 簡単なCRUD処理を実装する。 記事の管理を想定。 ユーザー認証機能はなし。 開発環境 PC: MacBook Pro(Apple M1, 2021) OS: macOS Big Sur 11.4 フロントエンド: Node.js 16.10 React 17.0.2 Typescript 4.1.2 バックエンド: Python 3.9.7 FastAPI 0.70.0 執筆の経緯 自分は普段フロントエンドをやらせていただいている身ですが、ここ最近バックエンドの仕組みを知りたくなってきた、かつ担当範囲を広げたい気持ちが高まってきており、バックエンドも今回のアドベントカレンダーをきっかけに、今後ちゃんとやっていかないと、と考えるようになりました。 そんな流れでバックエンド殆どやっていない自分でも扱えそうなWeb frameworkを探していたところ、FastAPIで書いていくことに決めました。その理由が 元々1年くらい前に興味本位でPython3を書いていた時期があった。 型定義前提になっている(Python3で型定義できる事自体、FastAPIに触れてから初めて知りました)。 小さい規模から扱える。 ドキュメントがかなり充実しており、公式からの情報提供も多い。 また今回アプリを作成するにあたり、フロント側は経験のあるReactを使いますが、せっかくなので使ったことのないライブラリ/やったことのないディレクトリ構成を試しました。 作成したアプリの概要 作成したアプリのリポジトリはこちら。 興味ある方はリポジトリをクローンして実際に動かしてみてください。 PRも大歓迎します。 使用フレームワーク React(フロントエンド) フロントエンド側はReact + TypeScriptで作成しました。 以下使用したライブラリとなります。 axios(APIのfetch用) react-helmet-async(ページタイトル指定) React Query(State管理) React Router(ルーター周り、最近リリースされたばかりのversion6系を使用) Jest/Testing Library(ユニットテスト) CSS周りについてはUIライブラリやフレームワークは使わず、SCSSで統一しました。 また今回の作成にあたり、ディレクトリ構成やライブラリ選定については以下のリポジトリを参考にさせていただきました。後ほどディレクトリ構成も紹介しますが、ほぼ踏襲した形にしています。 解説記事はこちら。 FastAPI(バックエンド) FastAPIはPythonのWeb frameworkの1つとなります。 PythonのWeb frameworkといえばDjango/Flaskが有名ですが、こちらは2018年に誕生と比較的歴史が浅いにも関わらずGitHubのスター数が4万近く(2021/11時点)と今大きく伸びているWeb frameworkです。 その大きな特徴として Pythonの型定義機能をベースに実装(型定義の仕方自体がTypeScriptに近いので個人的にも馴染みやすい)。 シンプルな方法でAPIを定義/実行できる。 APIサーバーを立ち上げるとSwagger UIが自動生成され、定義したAPIをすぐ参照できる。 今回実装したアプリのSwagger UIを立ち上げた様子です。「Try it out」ボタンを押下することで、実際にAPIを実行させることも可能です。 API定義の仕方 シンプルなGETメソッドの実装 まずmain.pyにsimpleという単純な文字列を返すメソッドを定義します。 main.py from fastapi import FastAPI app = FastAPI() @app.get("/") async def simple(): return {"message": "Hello World!!!"} そして次のコマンドを叩くとAPIサーバーが起動します(ちなみに--reloadを指定しておくことでpyファイルの更新のたびにAPIサーバーも更新されるようになります)。 bash uvicorn main:app --reload するとこのURLでAPIサーバーが立ち上がったよのメッセージが出るので、http://127.0.0.1:8000 のリンクに飛ぶと次のJSONが表示されます。 JSON {"message": "Hello World!!!"} 軽くAPI実装するだけならこれだけで実装できます。 その他のAPI実装パターン 上記で紹介したパターン以外のAPI実装の仕方については以下の記事が詳しいのでこちらに任せます。FastAPIをガッツリ活用していきたい方は是非読んでください。 公式ドキュメントも(一部英語残っていますが)日本語訳されており、チュートリアルが充実しているので上記記事と並行して読むことをお勧めします。 その他以下の資料も参考にしました。 Developing a Single Page App with FastAPI and React サンプルコード FastAPI - A python framework | Full Course ※YouTube動画 サンプルコード また自分からもFastAPIの勉強がてら実装した例集も貼っておきます。 その他使用したライブラリ/ソフト pytest(ユニットテスト) SQLite(データベース) SQLAlchemy(ORM) TablePlus(データベース操作用のソフト ※無料版) ディレクトリ階層 ディレクトリ階層は以下の形にしました。 . ├── backend <!-- バックエンド用ディレクトリ --> └── frontend <!-- フロントエンド用ディレクトリ --> バックエンド backend ├── app │ ├── tests <!-- ユニットテスト用 --> │ │ ├── __init__.py │ │ └── test_api.py <!-- api.pyのユニットテストコード --> │ ├── __init__.py │ ├── api.py <!-- APIの定義 --> │ ├── database.py <!-- sqlite設定用 --> │ ├── schemas.py <!-- APIにリクエスト値として設定するschemaを定義 --> │ └── models.py <!-- APIから返却されるmodelを定義 --> ├── app.db <!-- sqlite用ファイル(ローカルサーバー用) --> ├── test.db <!-- sqlite用ファイル(ユニットテスト用) --> ├── main.py <!-- FastAPIの立ち上げ用ファイル --> ├── README.md └── requirements.txt フロントエンド 今回はcreate-react-appコマンドで生成しての実装だったため、直接編集していないファイル/ディレクトリの記載は省略します。 featuresフォルダに関しての詳しい役割は、前述のbulletproof-react解説記事を参照にしてもらえばと思います。 frontend └── src ├── components <!-- コンポーネント置き場 --> │ ├── Elements <!-- buttonやlinkなど汎用的に使うコンポーネント置き場 --> │ ├── Header │ └── Layout ├── features <!-- アプリケーション各機能の名称のディレクトリを設置 --> │ ├── articles │ └── sample ├── lib <!-- 外部ライブラリ設定 --> │ └── axios.ts ├── routes <!-- ルーター設定 --> │ ├── index.ts │ └── public.ts ├── App.scss <!-- 今回のcssクラスをすべて定義 --> └── App.tsx <!-- メインのtsxファイル --> 実装で苦労したこと 正直今回はバックエンドよりずっとフロントエンドの実装に苦労しました。 バックエンドはほぼ公式ドキュメントだけで基本解決でき、UPDATEメソッドの実装で少し困った程度で済んだのですが、フロントエンドに関しては 参考元のbulletproof-reactの理解に時間がかかった。 React Queryについては使用したのが初めてかつチュートリアルに触れずにやった状態だったため使い方の理解に苦労した。 React Routerにおいて、ベタに<Routes>〜</Routes>で囲うのではなく、useRoutesを使った実装自体が初めてで模索しながらの実装だった。 慣れてない状態で、ベースとなるコンポーネントをAtomic Design寄りな設計にした。 の4点があったことが原因です。 ただ実装方法の理解に苦労した分、以下の恩恵も得ることができました。 今まで存在は知っていたが使う機会がなかったライブラリを初歩程度とはいえ一応扱えるようになった(React Queryなど)。 Atomic Design寄りなコンポーネント設計に沿った実装を一通り理解した。 bulletproof-reactの理解を通じて、これまであまり手を付けてこなかったReal World系の他リポジトリの理解も深めようと思ういい機会になった。 今後やっていきたいこと フロントエンド Reactはそこそこ書いてきたつもりでしたが、今回初めて知った便利なライブラリも多く、React界の奥深さを改めて感じました。 課題を踏まえて、今後やりたい/改善させたいことは以下の通りです。 React Routerについては、Version 6になって新しく登場したメソッドが追いきれてない。公式APIリファレンスを参考にしながら今後もスムーズに扱えるようになっていきたい。 同じ会社から最近でたRemixというフルスタックフレームワークも参照元としては良さそう。 バックエンドのユーザー認証が一通り扱えるようになったら、フロントエンドもログイン機能、ログインの有無でレイアウトを変えるなどのロジックを入れていきたい。 時間的制約で断念したライブラリの導入を今後行っていきたい。 E2Eテスト用のCypress。 コンポーネント管理用のStorybook。 今回作成したベース用のコンポーネントは徐々に改善させつつ今後使い回せるようにしたい。可能ならnpmパッケージにもしたい。 Material-UI改めMUIは今回の実装に近い書き方になっているため参考になりそう。 バックエンド いわゆるCRUD機能しか触っていない状態のため、まずは「ユーザー認証実装を学ぶ」→「freeCodeCampのAPIフルコースを完走する」の順に考えています(※いずれもReactなどフロントエンドのフレームワークとセットで実装する予定)。 ユーザー認証の実装 参考にしたページにも貼ってあるFastAPI - A python framework | Full Courseでは後半ユーザー認証を使った実装が紹介されており、まだ取り組めていないのでこれをベースに学習する予定です。 JWT (JSON Web Token)とかOAuth2など全然わかっていないので、概要を理解しつつ実装できるようになりたいです。 freeCodeCampの動画 海外ではかなり有名なfreeCodeCampのYouTubeチャンネルで最近FastAPIを使ったPythonのバックエンド実装の動画(なんと19時間超え!)が公開されたので時間をとってこれをまず完走しようと思っています。 動画の内容を一部抜粋すると以下になります。全編無料で本当にいいのか疑うレベルの充実ぶりです(バックエンド経験全然な自分が乗り越えられるか正直不安でしかない…)。 Postgres SQLへの接続/参照 SQLAlchemyとPostgres SQLをセットで使う Herokuを使ったデプロイ Docker周りの設定 GitHub Actionsを使用したCI/CDの設定 本記事は以上となります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React学習約2週間で、Reactの実務に参加した話

はじめに とある企業のアルバイトでReactを書いています。 幸運にも、Reactを学び始めてすぐにアルバイトを始めることができました。 その時の事や働き始めを思い出しながらまとめてみようと思います。 これからReactを学習する人やインターン参加したい人に少しは参考になるかなと思います。 自分について 自分は現在文系の大学3年生です。 プログラミング学習歴としては、1年半~2年の間くらいって感じです。 Reactを学び始めた当時は歴1年位でした。 4月から参加したのですが、3月3週目までは、JavaとかjQueryとか使うアルバイトに参加していました。 その時のことも少し記事にまとめてます 記事:めっちゃ初心者が地元の長期インターンに10ヶ月参加してみて感じたこと。 汎用的なスキル、基礎知識大事だって思って昔は資格の取得とかも頑張ってました。 記事:文系大学2年生が1年間で取得した資格5つ紹介 ちなみにアルバイトと資格取得に没頭していたので、この段階ではGitあまり使えなかったです。 2月途中まで、草ほぼ0です。 なので、ReactとGitを平行で学習って感じです。 (2月はJavaとGitを平行で学習してました) 会社について 学生が起業した会社です。 サービスリリース2ヶ月後に僕は参加しました。 エンジニアは、4名で全員が学生です。 フロントエンドとバックエンド別れて2名ずつで開発しています。 他部署も合わせると、当時は7名で現在は17名です。(ほぼ学生) 参加まで Reactを学びたいと思って学習を始めた頃に「うちでやらないか」と声を掛けて頂いて参加しました。 その声を掛けてくださった方とは、元々知り合いでした。 自分がプログラミングの学習を始めた頃に、自分からTwitterでDMを送って会いに行ったことがあり、それからTwitterでゆるーく繋がっていた感じです。(たまにリプしたりw) Twitter頑張るのって大事ですね。 初めて知り合ってから約1年後、アポイントとって会いに行き、久々にお話しに行きました(雑談)。 近況報告みたいな感じで、「ReactやGit学びたいから、今のアルバイトの会社を辞める予定で、とある会社(別の知り合いの人)にアルバイトのお願いしようと思う」 みたいな話をしました。 その時に、うちもReact使っていて、人を探してるみたいな話を聞きました。 別の知り合いの人へ話をしに行ったりもしたのですが、色々考えて、結果的に、誘って頂いた今の会社に参加することになりました。(Twitterで繋がった人) 学習について ここでは、参加するまでにどんな学習をしていたのかを紹介します。 学習の仕方 公式チュートリアルを軽くやる 静的なサイトを作ってみる Todoアプリを作ってみる 色々気になる部分を学んでいく... みたいな感じでした。 Todoアプリを作る前に、静的なサイト(部活用のホームページ的な奴)を作ったのはよかったかなと思います。 段階を踏んで理解していけます。propsとかstateの使い方、jsxの構文とかが使えるようになりました。 実際に手元で動かしてみるがやはり最強です。 ポイントは、なんでもいいから自分で変更を加えるだと思っています。 文言変えたり、処理を少し変更してみたり、機能を1つ加えてみたり、なんでもいいと思います。 写して動かすだけだとやっぱり分からないので!! 最初はよく分からなくて、classコンポーネントと関数コンポーネント両方学んだりしてたのですが、とりあえずは、関数コンポーネントだけ学べば大丈夫です。 あとは、よく分からなくてもTypeScriptで書いた方が絶対いいです。 あと、Redux入れると言語変わるレベルで、複雑になって変わってくるので、最初は学ばなくてもいいかなって思っています。 propsの使い方とか、hooksの使い方とか基礎的な部分に集中したほうがいいと思います。 学習に使ったもの React公式ドキュメント Next.js公式ドキュメント IT KINGDOM JavaScript Primer Qiita YouTube React公式ドキュメント 基本的に気になる部分があった時や深く理解したい時に読むとかそういう使い方です。 classコンポーネントで書かれたりして好きでは無かったんですが、最近関数コンポーネントで書かれてるみたいですね。 とりあえず、チュートリアルはやってみました。 これも確かclassコンポーネントで書かれていた気がします。 Qiitaの記事とかで、「関数コンポーネントに書き換えてみた」とかあると思うので、それを参考にしてもいいかもしれません。 Next.js公式ドキュメント めっちゃすごいって話を聞いて面白そうって思って脱線してしまいましたw でも基本的なReactの構文みたいなところは同じで学べます。 むしろ、色々といい感じにやってくれてるので、Reactのルーティングとか設定とか、そこら辺を苦労せずに効率的に基礎的なReactの構文みたいなところを学べた気がします。 チュートリアルの日本語訳とりあえずこれをやりました。 IT KINGDOM Twitterで知り合った同じ23卒の人と3人でzoomしている時に、2人が入っていて自分も入りました。 だいぶ良いです。 いい所ポイント 実際のプロが書いたコードが見れる (真似とかしてました) サロンオーナーに相談とかできる newsとか良記事とか、色々な最新情報が紹介されている 解説動画めっちゃあって、めっちゃわかりやすい ここの解説動画で、React, Next.jsの基本的なことはできるようになるんじゃないかなって思ってます。 JavaScript Primer JavaScriptの基礎をざっと学び直したくて、使っていました。 部活までの時間とか、電車の時間とかにiPadを使って読んでいました。 気になった部分、不安な部分を一気にざっと学ぶのに使いました。 気になる部分だけざっと読んだらもう使っていません。 Qiita React hooksを基礎から理解するシリーズの記事は何回か読んだ記憶があります 移動中とかトイレとか、部活の待ち時間とか、ちょっとした時間に読むみたいな感じでした。 他にもいい記事を見つけたら読むみたいな感じで、会いてる時間に色々読んでいました。 YouTube しまぶーのIT大学 【とらゼミ】トラハックのエンジニア学習講座 -新・日本一わかりやすいReact入門【基礎編】 ※ 僕が見てたのはこれの古いバージョンです。新しいやつが出てるようだったのでそっちを紹介しておきます。 やっすんのエンジニア大学-React入門 未経験から1週間でマスターする お風呂の時間とかにたまに見たりしていました。 全部は見ていないと思いますが、見たいなと思ったものを2倍速にして、流し見みたいな感じです。 しまぶーのIT大学さんはサロンメンバー限定公開で沢山動画があって、それもすごいわかりやすいです。 それ見て、あとは手を動かせば基礎的な大体のことできるんじゃないかなと思います。 参加してから フロントエンドエンジニアは僕と同学年の人の学生2人だけなので、「初めは教えてもらいながら開発」とかではありませんでした。調べて、コード読んで、動作確認して、なんとか開発していくみたいな体制です。 業務委託契約で、好きな時間に開発って感じなので、質問したらすぐ教えてもらえるとかでも無いですが、相談とかは全然できる感じです。基本的には、ドキュメント読んで、コード読んで、ゴリゴリって感じです。 参加してまず感じたことは、約2週間学んだレベルでもやること、出来ることたくさんあると感じました。 例えば、ページ単位でのバグ改善や表示の修正。 ちょっとした凡ミスが原因ってことが多いです。 該当箇所に辿り着くのは、最低限のReactの知識があって、少しプロジェクトの構成やルールを教えて貰えれば、できると思います。 初めは、実際に表示されている文言をVSCodeの検索にかけてファイルを見つけたり、 文言変更して表示を確認して、対象のファイルかどうか確認したりしていました。 該当の箇所が見つかったら、console.logとか使って実際に値を確認しながら、おかしそうなところを探して直すみたいな感じでした。 1から作るのは難しいと思いますが、改修系は難易度が低い(だけど重要みたいな)ものも多いです。 また、機能追加とかも、参考にできるコードがあるので落ち着いてコードを読みながら書いていけばそこまで難易度高くないことも多いと感じました。 そんな感じで、難易度低めのタスクをいくつかこなすとどんどん慣れていきます。 Reactの知識もそうですが、そのアプリ自体の知識とか、アプリの構成に関する知識とかがついてきて、どんどんできることが増えていきます。 って感じで実際に、参加した月の間には、新しい(機能)ページを作ったり、改善提案して改善とかもしていました。スタートダッシュよかったなっと思っています。 自分がうまく行ったなと思う要因は2つです。(結構当たり前なことです) アプリを色々触って、どんな機能があるか把握できたこと。 始まる前にコードもらって読んだりしていたこと。 アプリにどんな機能があるか把握することと、コード全体をみてどんな役割でディレクトリが分けられているのかを把握することで、開発の効率はすごく上がると思います。実際にタスクやっている時は、該当部分に集中してしまいますが、時間をとって全体の把握はすごく大事だなと思いました。 その後もどんどん開発をしていき、機能追加だったり、SEO対策だったり、僕主導で技術移行なんかもやったりしました。どんどんできることが増えていっていき、楽しいです。 達成したいことがあって、それを達成するために、調べて、コードを書いて、改善してを繰り返し、少しずつ成長していっています。 おわりに React学習2週間くらいで、実務に参加している話を振り返りながら書きました。 本当に学習を始めたばかりの時って働くこととか開発することのイメージ全くわかないと思うので、少しでも参考になったら嬉しいです。 今はその会社と、このQiitaを作っている会社でインターン生として開発をしているのですが、「できるね」って言ってもらえることもあったりして、Reactの力はだいぶ着いて来ているなと思っています。 これからもどんどん成長していきたいです。 一緒に頑張っていきましょ!!^^ あと、初アドベントカレンダー参加でした。 記事書くって宣言すると、めんどくさくてもちゃんと書くので宣言って大事ですね。 ありがとうございました。 参考リンク一覧 めっちゃ初心者が地元の長期インターンに10ヶ月参加してみて感じたこと。 文系大学2年生が1年間で取得した資格5つ紹介 React公式ドキュメント Next.js公式ドキュメント IT KINGDOM JavaScript Primer React hooksを基礎から理解する しまぶーのIT大学 【とらゼミ】トラハックのエンジニア学習講座 -新・日本一わかりやすいReact入門【基礎編】 やっすんのエンジニア大学-React入門 未経験から1週間でマスターする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fluid Frameworkを使って共同編集可能なお絵描きアプリに挑戦する

最近Teamsで共有されたファイルを直接編集することが多く、その際の共同編集体験が良かったので調べてみました。 どうやらFluid Frameworkというマイクロソフト謹製のライブラリーが用いられているとのことです。こちらオープンソース化もされています。 公式サイトでは付箋アプリなどおもしろいサンプルも提供しています。 難しいこともいろいろと書いてあるので手を動かして体験してみようと思います。 テキストの共有はサンプルもあったので画像にしてみようかと共同編集可能なお絵描きアプリに挑戦してみます。 Fluid Framework その前に簡単にFluid Frameworkとは。 What is Fluid Framework? Fluid Framework is a collection of client libraries for distributing and synchronizing shared state. These libraries allow multiple clients to simultaneously create and operate on shared data structures using coding patterns similar to those used to work with local data 引用(https://fluidframework.com/docs/#what-is-fluid-framework) 一言でいうと各クライアントの状態を共有、同期するためのクライアントライブラリーです。 特徴的な点はクライアントの状態を共有してマージするのは各クライアントで行う点です。 想定されている流れは以下の通りです。 The following is a typical flow. Client code changes data locally. Fluid runtime sends that change to the Fluid service. Fluid service sequences that operation and broadcasts it to all clients. Fluid runtime incorporates that operation into local data and raises a “valueChanged” event. Client code handles that event (updates view, runs business logic). 引用(https://fluidframework.com/docs/#how-fluid-works) 大きく分けてFluid ContainerとFluid Serviceのふたつの要素で構成されています。 Fluid Containerはローカルの編集状態をFluid Serviceに送り、Fluid Serviceはシーケンス化した操作を各クライアントへ配布します。 Fluid Frameworkはサーバーでは操作のシーケンスを配布するだけで、状態のマージは各クライアントが行います。その結果遅延時間を劇的に削減しているとのことです。 他にも興味深い内容が書かれていますので公式サイトをぜひご覧ください。 お絵描きアプリ開発 今回は下記を参考にReactで作成します。 環境 node -v > v16.7.0 yarn -v > 1.22.11 npx create-react-app --version > 4.0.3 Reactの構築 npx create-react-app fluid-canvas --template typescript cd fluid-canvas typescriptのバージョンは4.1.2です。 Fluid Framework インストール yarn add @fluidframework/tinylicious-client fluid-framework バージョンは0.52.1です。 fluid-framework Fluid Framework本体。クライアント間のデータを同期するためのライブラリーです。 @fluidframework/tinylicious-client Tinyliciousは開発目的のローカルのインメモリーを用いたFluidサービスです。 Tinylicious ClientはTinyliciousへの接続やFluid Containerのスキーマを定義します。 本来はFluid Loaderや接続手順、データ取得の手続きなどを構築しないといけなさそうなんですがカプセル化してくれています。 Fluidサービスは他にもAzure Fluid Relay用や独自構築できるRouterliciousが提供されています。 react-signature-pad-wrapper インストール yarn add react-signature-pad-wrapper 描画にはSignature PadをReact用にラッパーしたreact-signature-pad-wrapper(v2.0.2)を使用します。 Signature Padはスムーズな署名を描くためのライブラリーと謳っており、シンプルでアウトプットもPNG, JPEG, SVGに変換できて使い勝手が良いです。 お絵描き部分作成 App.tsx import React from 'react'; - import logo from './logo.svg'; - import './App.css'; + import SignaturePad from "react-signature-pad-wrapper" function App() { + const signaturePadRef = React.useRef<SignaturePad>(null); + return ( + <SignaturePad ref={signaturePadRef} /> + ); - return ( - <div className="App"> - <header className="App-header"> - <img src={logo} className="App-logo" alt="logo" /> - <p> - Edit <code>src/App.tsx</code> and save to reload. - </p> - <a - className="App-link" - href="https://reactjs.org" - target="_blank" - rel="noopener noreferrer" - > - Learn React - </a> - </header> - </div> - ); } export default App; signaturePadRef は描画した絵を取得、更新する際に使用します。ついでにlogo.svgなどは削除しました。 起動して動作確認します。 yarn start マウスドラッグで絵が描けるようになりました。 本家ではクリアや色変更などのデモもあります。もっとリッチにできますが今回はこのまま進めます。 Fluid Framework導入 App.tsx import React from "react"; import SignaturePad from "react-signature-pad-wrapper" + import { TinyliciousClient } from "@fluidframework/tinylicious-client"; + import { SharedMap } from "fluid-framework"; TinyliciousClient と SharedMap をimportします。SharedMap はFluid Frameworkが提供するDDS(distributed data structures)の一種で、Key-Valueデータを提供します。 DDSはローカルデータを扱うように操作して、各クライアント間で状態を共有することができます。 コンテナーの生成または取得 App.tsx import React from "react"; import SignaturePad from "react-signature-pad-wrapper" import { TinyliciousClient } from "@fluidframework/tinylicious-client"; import { SharedMap } from "fluid-framework"; + const dataKey = "drawing"; + const containerSchema = { + initialObjects: { view: SharedMap } + }; + const client = new TinyliciousClient(); + const getViewData = async (): Promise<SharedMap> => { + let container; + const containerId = window.location.hash.substring(1); + if (!containerId) { + ({ container } = await client.createContainer(containerSchema)); + const id = await container.attach(); + window.location.hash = id; + } else { + ({ container } = await client.getContainer(containerId, containerSchema)); + } + return container.initialObjects.view as SharedMap; + } function App() { ... } dataKey はShardMapに設定するKeyです。 containerSchema はFluid Containerの定義です。initialObjectsはコンテナーの作成時に作成され、コンテナーが有効な間存在します。各クライアントはinitialObjects を介してアクセスし、分散された状態を共有します。 他にDynamic objects(dynamicObjectTypes)があります。Dynamic objectsはアプリで扱うデータサイズが大きいため遅延ロードしたり、必要なデータがユーザーの操作に依存する場合にUXを守るために利用するようです。 getViewData はコンテナーからShardMapを取得します。コンテナーが未作成の場合は新しくコンテナーを作成し、コンテナーIDをURLハッシュに設定します。URLハッシュにコンテナーIDが設定されている場合はコンテナーを取得します。 サンプルではURLハッシュへのアクセスがlocation.hashになっていますが、eslintのno-restricted-globalsに引っかかるのでwindow.location.hashに変更しています。 Fluidデータの取得 App.tsx function App() { const signaturePadRef = React.useRef<SignaturePad>(null); + const [fluidData, setFluidData] = React.useState<SharedMap>(); + React.useEffect(() => { + getViewData().then(view => setFluidData(view)); + }, []); ... } useEffectでgetViewDataをコンポーネント生成時に1度だけ呼び出します(第2引数に空配列を設定)。 fluidDataにcontainerSchema で定義したview(ShardMap)を設定しています。 Fluidデータの同期 App.tsx function App() { ... + React.useEffect(() => { + if (!fluidData) { + return; + } + + const syncView = () => { + if (signaturePadRef.current) { + signaturePadRef.current.fromDataURL(fluidData.get(dataKey) as string); + } + } + syncView(); + fluidData.on("valueChanged", syncView); + return () => { fluidData.off("valueChanged", syncView) } + }, [fluidData]); ... } useEffectをもうひとつ追加してFluidデータとUIを同期します。UIの更新処理はsyncViewにてfluidDataの値を取得して、Signature PadのfromDataURLに設定します。 また、Fluidデータは別クライアントが変更する可能性があります。Fluidデータに変更があるとvalueChanged イベントが発生するので、syncViewでUIの更新処理を行います。 Fluidデータの更新 App.tsx function App() { ... + const [imageData, setImageData] = React.useState<string>(); + React.useEffect(() => { + if (imageData) { + fluidData?.set(dataKey, imageData); + } + }, [imageData, fluidData]); + const onEnd = React.useCallback(() => { + const signaturePad = signaturePadRef.current; + const dataUrl = signaturePad?.toDataURL("image/svg+xml"); + setImageData(dataUrl); + }, [setImageData]); return ( - <SignaturePad ref={signaturePadRef} /> + <SignaturePad ref={signaturePadRef} options={{onEnd: onEnd}} /> ); } ローカル用にimageDataを用意します。imageDataに変更がある場合はfluidDataにも設定し、別クライアントへ変更を共有します。 onEnd はSignature Pad の描画終了時にコールされます1。Signature PadのtoDataURLの値をimageDataに設定します。 お絵描きアプリ実行 Fluidサービス(サーバー)としてtinylicious を起動します。 npx tinylicious@latest アプリを実行します。 yarn start 2つのブラウザから同じURLにアクセスして描画が同期しています。 複数端末からアクセスしてみる ただし、tinylicious はローカルでしかアクセスできないので別端末からもアクセスできるようにしてみます。公式にてngrok を使ってみよ、とあるのでやってみます。 アカウント登録や認証用設定が必要なので公式の手順を参照ください。 ngrok http実行後にForwardingに表示される転送用のドメインをTinyliciousClientProps のconnection のdomain に設定します。設定したTinyliciousClientProps をTinyliciousClient の引数にします。 App.tsx + const clientProps: TinyliciousClientProps = { + connection: { port: 443, domain: "https://forwarding-domain.ngrok.io" } + } - const client = new TinyliciousClient(); + const client = new TinyliciousClient(clientProps); httpで設定する場合はportを80にしてください。 npx tinylicious@latest ngrok http 7070 yarn start tinyliciousのPortのデフォルトは7070です。 わかりづらいですが2つの端末から描画しています。 まとめ クライアントロジックだけで共同編集機能を実現しているFluid Frameworkはとても強力です。 今はバージョン1ではなく、「まだ製品品質のソリューションを提供できる状態ではありません」とのことなので今後が楽しみです。 今回作成したソースはこちらです。 本記事を書いている最中にSignature Padが3年ぶりのメジャーアップデートをしたようでonEndがなくなっていました。react-signature-pad-wrapperの方もそのうち対応するかもしれません。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React,react-hooks,closure]そのstate更新がうまくいかないのは、stale-closureのせいかも?

はじめに Reactで非同期処理を用いてStateを更新する際、処理の書き方に気をつけないとクロージャの性質によって最新の値を基にStateが更新されないことがある。 useEffectで値のSubscribeをしてるときなどは、特に注意が必要である。(筆者はこれでハマったため) 本稿は、stale-closure(古くなったクロージャ)が引き起こすstate更新での問題とは何か?とその解決策を解説する。 stale-closureによってどんな問題が起きるか 例えば、以下のようにsetIntervalを用いて1秒ごとにカウントアップを行うプログラムがあったとする。 CodeSandboxはこちら import { useEffect, useState } from "react"; export default function App() { const [count, setCount] = useState(0); const countUp = () => { return setInterval(() => { setCount(count + 1); }, 1000); }; useEffect(() => { countUp(); }, []); return <div>{count}</div>; } このプログラムは1ずつカウントアップしていくことが期待されるが実際にはそうはならず、一度1に上がった後それ以上カウントアップされない。 期待される挙動: 0から順に1ずつカウントアップされる 実際の挙動:カウントは1以上にならない (ここでは非同期処理の例としてsetIntervalを使っているが、更新用の関数の作成タイミングと実行タイミングがずれていることが重要で、頭の中で何らかの非同期処理と置き換えてもらうとご自身のユースケースと重ねやすいかもしれない.) countステートはAppコンポーネントに定義されているし、countUp関数はそのステートを間違いなく参照しているように見えるのになぜこうなるのか?? 原因 この問題を引き起こしているのはstale-closure、つまり古くなった(鮮度の落ちた)クロージャである。 クロージャとは ここで一度Javascriptのクロージャについて軽くまとめておく。 クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせです。言い換えれば、クロージャは内側の関数から外側の関数スコープへのアクセスを提供します。JavaScript では、関数が作成されるたびにクロージャが作成されます。 出典:https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures これだけだとよくわからないが、以下の説明でしっくりきた。 クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆる変数によって構成されています。 出典:同上 つまり、クロージャは関数を作成した時点での変数の値と関数を保持しているものなのである。 今回のコードでの問題点 const countUp = () => { return setInterval(() => { setCount(count + 1); }, 1000); }; 上の部分で、countUp関数を作成する際にsetInterval関数の中にあるcountはクロージャに捕らえられてしまい、この時点でのcountの値(=0)を保持し続けることになってしまっていたのである。そのため、setIntervalで何度setCountが実行されようと 0+1を延々と繰り返し、カウントアップできなかったのである。 詳細な流れ 1.初期描画(countUp作成) 2.useEffect実行(countUp呼び出され、setIntervalが実行される) 3.+1秒後 0(初期値) + 1 = 1 -> setState 4.再描画 ここでstale-closure発生!! dependenciesを[]にしているため、再描画されてもuseEffectは実行されない。 そのため再描画で新しく作られたcountUpではなく、初期描画時のcountUpが使われる. そしてこのcountUpはクロージャによって古い値を参照したままになっている) 5.+1秒後 0(初期値) + 1 = 1 -> setState stateの値が変わらないため、以後再描画はされない 6.+1秒後 0(初期値) + 1 = 1 -> setState ... n.+1秒後 0(初期値) + 1 = 1 -> setState useState単体使用の例 上記ではuseEffectを例として出したがuseState単体を用いても非同期処理が絡めば、stale-closureが悪さをすることはある。 CodeSandboxはこちら。 import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); const dummmy = () => { setTimeout(() => { setCount(count + 1); }, 1000); }; return ( <> <div>{count}</div> <button onClick={dummmy}>Button</button> </> ); } 上記のコードで素早くボタンを複数回押すと、カウントアップは1ずつしかされないことが確認できるはずである。 素早くとしているのは、もし連打ではなくカウントアップ(state更新)を待ってからボタンを押した場合は、state更新に伴う再描画により関数も再作成されて、新しいクロージャが作成されstale-closureが発生しないためである。 対策 さて、ここまででstale-closureがstate更新でどのように悪さをするかは述べた。 では、どのようにすればこのstale-closureを回避できるのか。 ここでは2つの方法を紹介する。(他にもあるとは思うのでコメントお待ちしてます) ※useEffectにdependenciesを設定する方法もあるかとは思うが、setIntervalの例では、setIntervalを実行するたびにタイマー処理が追加されていき,countの増加が不自然になったので今回は例としてわかりづらいので割愛) useState(prevState=> {}) を利用する クロージャに捕らえられていた古い値を参照していたのが今回の問題なのでuseState関数の機能を使って常に最新のstateを参照して処理を行うようにする。 ※useStateはuseState(prevState=> {})のように記述することで最新のstateを参照して任意の処理を行うことができる CodeSandboxはこちら。 import { useEffect, useState } from "react"; export default function App() { const [count, setCount] = useState(0); const countUp = () => { return setInterval(() => { console.log(count); setCount((prevState) => prevState + 1); }, 1000); }; useEffect(() => { countUp(); }, []); return <div>{count}</div>; } useReducerを使う useReducerを使い、更新処理関数の中ではdispatchを行うだけにする。 これにより、closureが新鮮かどうかを気にせず常にreducerで最新のstateを基に処理を行うことができる CodeSandboxはこちら。 import { useEffect, useReducer, useState } from "react"; const reducer = (state, action) => { switch (action.type) { case "COUNT_UP": return state + 1; default: return state; } }; export default function App() { const [count, dispatch] = useReducer(reducer, 0); const countUp = () => { return setInterval(() => { dispatch({ type: "COUNT_UP" }); }, 1000); }; useEffect(() => { countUp(); }, []); return <div>{count}</div>; } 実践的なユースケース 随時更新 ここまでで、stale-closureに気をつけてstate更新をする手法をsetIntervalやsetTimeoutを用いて説明してきた。以下ではもう少し具体的にこの知見が活かせるケースを紹介する。 というよりも、実は筆者が以下の処理を技術書で学習しているときに「useStateだとclosureが悪さするからuseReducerにしているよ」と言った記述があり、何を言っているんだ?となったのがこの記事執筆のきっかけである。 subscription処理 以下にAmplifyでデータの追加を検知してstate更新を行うアプリケーションの一部を抜粋して掲載する. Amplifyを具体的に出しているが、Amplifyに限らず購読処理を行う場合であれば本稿の対策が同様に当てはめられるはずである。 import {OnCreatePostSubscription, Post} from "./API"; import {API, graphqlOperation, Storage} from "aws-amplify"; import {useEffect, useReducer, useState} from "react"; import {listPosts} from "./graphql/queries"; import {isDefined} from "./utils"; import {GraphQLResult} from '@aws-amplify/api-graphql' import {onCreatePost} from "./graphql/subscriptions"; interface SetPosts { type: 'SET_POSTS', posts: ClientPost[] } interface AddPost { type: 'ADD_POST', post: ClientPost } type Action = SetPosts | AddPost type ClientPost = { id: string; imageKey: string; title: string; imageUrl: string; } const reducer = (state: ClientPost[], action: Action) => { switch (action.type) { case 'SET_POSTS': return action.posts; case "ADD_POST": return [action.post, ...state] default: return state; } } const getSignedPosts = async (posts: Post[]): Promise<ClientPost[]> => { const signedPosts = await Promise.all( posts.map(async (item) => { if (!item.imageKey) return; const signedUrl = await Storage.get(item.imageKey) const clientPost: ClientPost = { id: item.id, imageKey: item.imageKey, title: item.title, imageUrl: signedUrl }; return clientPost }) ) return signedPosts.filter(isDefined); } type PostSubscriptionEvent = { value: { data: OnCreatePostSubscription } }; const Posts = () => { const [posts, dispatch] = useReducer(reducer, []) const update = async ({value: {data}}: PostSubscriptionEvent) => { const newPost = data.onCreatePost; if (!newPost || !newPost.imageKey) return; const signedUrl = await Storage.get(newPost.imageKey) const newClientPost: ClientPost = { id: newPost.id, imageKey: newPost.imageKey, title: newPost.title, imageUrl: signedUrl } dispatch({type: 'ADD_POST', post: newClientPost}) } useEffect(() => { fetchPosts(); const client = API.graphql( graphqlOperation(onCreatePost) ) if ("subscribe" in client) { const subscription = client.subscribe({ next:update }) return () => subscription.unsubscribe(); } }, []) const fetchPosts = async () => { const postData = await API.graphql(graphqlOperation(listPosts)) as GraphQLResult<{listPosts:{items:Post[]}}> const items = postData.data?.listPosts.items; if (!items) return; const signedPosts = await getSignedPosts(items) dispatch({type: 'SET_POSTS', posts: signedPosts}) updatePost(signedPosts) } return ( <div> <h2 style={heading}>Posts</h2> {posts.map(post => ( <div key={post.id} style={postContainer}> <img style={postImage} src={post.imageUrl} alt=""/> <h3 style={postTitle}>{post.title}</h3> </div> ))} </div> ) } const postContainer = { padding: '20px 0px 0px', borderBottom: '1px solid #ddd' } const heading = {margin: '20px 0px'}; const postImage = {width: 400}; const postTitle = {marginTop: 4} export default Posts; 注目して欲しいのはstate更新処理とuseEffectの部分である。 state更新処理(正確にはdispatchしてるだけ) const update = async ({value: {data}}: PostSubscriptionEvent) => { const newPost = data.onCreatePost; if (!newPost || !newPost.imageKey) return; const signedUrl = await Storage.get(newPost.imageKey) const newClientPost: ClientPost = { id: newPost.id, imageKey: newPost.imageKey, title: newPost.title, imageUrl: signedUrl } dispatch({type: 'ADD_POST', post: newClientPost}) } useEffectでSubscriptionの設定をしている部分 useEffect(() => { fetchPosts(); const client = API.graphql( graphqlOperation(onCreatePost) ) if ("subscribe" in client) { const subscription = client.subscribe({ next:update }) return () => subscription.unsubscribe(); } }, []) 上記はデータが追加された際にstateを更新するというSubscription処理だが、これは今回の解説で示してきたコードと同じくuseEffectの中でstateを更新する関数を非同期で実行するケースである。 したがって、subscribeで取得した値(state)の管理はuseStateではなく、useReducerを用いており、update関数ではdispatchするに留めて、常にreducerの中で最新のstateをもとに更新処理が行われている。 もし、これをuseStateで行った場合は、subscribeした値を初期値([])に加えるだけになり、stateは[subscribeした値]となる。 本来得たい(初期フェッチで得た値 + subscribeした値)の配列にはならない。 また、対策セクションで述べたように以下のようにuseState(prevState=>{})を利用しても更新可能である。 const newClientPost: ClientPost = { id: newPost.id, imageKey: newPost.imageKey, title: newPost.title, imageUrl: signedUrl } updatePost(prevState => [...prevState,newClientPost]) おわりに 本稿が皆さんの「なんでうまくstate更新されないんだ!!」問題の一助になれば 参考 Full Stack Serverless : Modern Application Development with React, AWS, and GraphQL Be Aware of Stale Closures when Using React Hooks 7 things you may not know about useState クロージャ (MDN)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Next.js/React】好きな本10選を作るWebアプリ作りました

ブックリストを作成し、オリジナル画像をSNSでシェアすることが出来るアプリを作りました。 URL: https://www.tennovels.com/lists/SANyAYlpvw0Y2KFM3RIX Twitter: @10novels 紹介動画:https://youtu.be/rAQD_ACqfjs 作ったもの アプリ概要 好きな本10選を作成し、オリジナル画像をSNSでシェアするアプリを作りました。ログイン、新規登録なしで使うことができます。 ローズウォーターさん、あなたに神のお恵みを/カート・ヴォネガットソラリス/スタニスワフ・レム1Q84(BOOK1(4月ー6月))/村上春樹銀の匙/中勘助細雪(上巻)改版/谷崎潤一郎沈黙/遠藤周作...#趣味は読書です@10novels より https://t.co/QltNFJwZWx— 趣味は読書です (@10novels) November 23, 2021 上のツイートのようなブックリストを作成しTwitterで画像をシェアすることができます。 タイトルと著者名は自動で記載されます。 文字数オーバーの場合は…と記載されます。 技術スタック こちらがインフラ構成です。 Front-end フロントエンドは Next/React。 Back-end バックエンドはFirebaseを使用しています。デプロイはVercel。 Firestore: データ管理 Functions: 設定したトリガーでバックエンド処理を自動的に実行 Storage: 画像を保存 無料プランではなく、従量制の Blaze プランを使用。 使用技術 Next/React Firebase (Firestore, Functions, Storage) Material UI Vercel Algolia Rakuten Total Books API
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SWRについてざっくりまとめた

この記事について SWRを調べて得た知見をざっくりまとめた記事。 キャッシュを管理するライブラリを使用するのは初めてだった為、その目線から感じた疑問や、この系統を調べると必ず出てくる「キャッシュ戦略」についてもやんわりまとめた。 この記事でまとめたこと SWRの概要について キャッシュ管理系ライブラリのざっくりとした概要・種類 stale-while-revalidateを利用したキャッシュ戦略について そもそもキャッシュ管理系ライブラリとは? この系統のライブラリの立ち位置としては状態管理の中に属する部類 状態管理の中でも、キャッシュの状態管理に特化したライブラリ キャッシュ管理ライブラリの特徴 データfetchを行ってくれて、fetchしたデータをキャッシュとして管理できる キャッシュを保存するだけでは無く、サーバ側とのデータの同期や更新も可能◎ データ取得、ローディング状態、エラーが発生した時をシンプルに記述できる キャッシュ管理のロジックを手書きする必要がなくなる 世の中には様々なキャッシュ管理ライブラリが存在する フレームワークに合わせて様々なキャッシュ管理ライブラリがある 全フレームワーク向け:RTK Query React向け:SWR / React Query / Apollo Client Vue向け:swrv SWR とは? Vercel社が開発したライブラリで、データフェッチやキャッシュを管理するためのライブラリ 名前の由来 RFC 5861 で提唱されたキャッシュ戦略である stale-while-revalidate の略称 ※下の方で記述していますがSWRでは全く同じ仕組みではなく、この思想に基づいた設計になってるイメージ stale-while-revalidate とは? HTTP ヘッダのCache-Controlに設定できるディレクティブの一つ(MDN) Cache-Control: max-age=600, stale-while-revalidate=30 // これのこと! stale-while-revalidate を設定した時の具体的なキャッシュを利用する流れ キャッシュがない場合とキャッシュがある場合でざっくり2パターンある キャッシュがない場合 1. サーバーにフェッチリクエストを投げる 2. レスポンスをキャッシュとして保存しクライアントへ即表示 キャッシュがある場合 1. 保存されているキャッシュをクライアントに表示させる 2. max-ageの期間以上になったら、サーバーにフェッチリクエストを送る 3. 差分があれば保存されているキャッシュを更新する 4. 何らかのリクエストがあったタイミングでそのキャッシュをクライアント側に表示 SWR と stale-while-revalidate で若干異なるところ キャッシュがある場合のキャッシュを表示するタイミングが若干違う stale-while-revalidate の場合 → 非同期でキャッシュを更新後、リクエストがあったタイミングで最新のキャッシュを表示する SWRの場合 → 非同期でキャッシュを更新後、即反映する なので、SWRの方がキャッシュの反映が早い!! (サンプル) SWR ができること 高速なページナビゲーション(キャッシュがある場合はそれをすぐ出す) 定期的・自動でポーリングする データをfetchするフックに関連付けられているコンポーネントが画面に表示されている場合にのみ、再フェッチが行われます。 フォーカス時・ネットワーク回復時の再検証 ローカルキャッシュの更新(mutate) リクエストの重複排除 React Nativeでも使用可能◎ SWR の使い方 めちゃめちゃシンプルに書ける!!! import useSWR from 'swr' function Profile () { // 第一引数にキャッシュキー、第二引数にfetcherを渡す。fetcherは事前に用意する必要がある // 第三引数にはoptionを渡せるが省略も可能 const { data, error } = useSWR('/api/user/123', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> // データをレンダリングする return <div>hello {data.name}!</div> } キャッシュキーとは? SWR内部のキャッシュを管理しているkey ユニークな文字列を設定する必要がある fetcherとは? fetchする関数がラップされている関数のこと(参考) イメージとしては、useSWRの第一引数に入ってくるURLを任意のライブラリでリクエストしてデータを取得してくる感じ。ライブラリはPromiseを返すものであればなんでもOK const fetcher = (url: string) => fetch(url).then(r => r.json()) axiosを使用する場合はこうなる import axios from 'axios' const fetcher = url => axios.get(url).then(res => res.data) function App () { const { data, error } = useSWR('/api/data', fetcher) // ... } GraphQLを使用する場合はこうなる import { request } from 'graphql-request' const fetcher = query => request('/api/graphql', query) function App () { const { data, error } = useSWR( `{ Movie(title: "Inception") { releaseDate actors { name } } }`, fetcher ) // ... } SWR の便利・お気に入りなところ Reduxで管理する場合と比べて管理しやすくなり最高 キャッシュの更新が簡単 パフォーマンスが最適化されている 1. Reduxで管理する場合と比べて管理しやすくなり最高 Reduxを使ってキャッシュ管理した場合 fetchを実行するsliceを作成する fetchを実行した結果をキャッシュとして保存するsliceを作成する sliceのカスタムフックを作成する → これをページで使用する 合計:3ファイル作成 SWRでキャッシュ管理した場合 useSWRを内包したカスタムフックを作成する → これをページで使用する fetchを実行+キャッシュを管理 合計:1ファイル作成 ファイル数が一気に減ったので管理しやすい!!!! 2. キャッシュの更新が簡単 POST や DELETE といった処理を行うと現在のローカルで持つキャッシュとサーバーサイドで持つデータの不整合が起きると思います。 この不整合を解消するためにローカルキャッシュの更新を行う必要があり、それを行ってくれるのがMutation サーバーサイドに強制的にrequestを送り再fetchすることが可能◎ import useSWR, { useSWRConfig } from 'swr' function App () { const { mutate } = useSWRConfig() return ( <div> <Profile /> <button onClick={() => { // クッキーを期限切れとして設定します document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;' // このキーを使用してすべての SWR に再検証するように指示します mutate('/api/user') }}> Logout </button> </div> ) } サーバ側の変更を待たずにローカルキャッシュを書き換えることも可能◎ import useSWR, { useSWRConfig } from 'swr' function Profile () { const { mutate } = useSWRConfig() const { data } = useSWR('/api/user', fetcher) return ( <div> <h1>My name is {data.name}.</h1> <button onClick={async () => { const newName = data.name.toUpperCase() // 第3引数にfalseを設定すると再検証をせずに直ちにローカルデータを更新します mutate('/api/user', { ...data, name: newName }, false) // ソースを更新するために API にリクエストを送信します await requestUpdateUsername(newName) // ローカルデータが最新であることを確かめるために再検証(再取得)を起動します mutate('/api/user') }}>Uppercase my name!</button> </div> ) } 3. パフォーマンスが最適化されている(参考) 最適化ポイントはいくつかあるものの、すごいと感じたのは2点 1. リクエストの重複排除 同じ内容のfetchを行ってるコンポーネントを同じ場所に何個も書いた場合、実際にrequestされるのはその個数分ではなく一回のみ! function useUser () { return useSWR('/api/user', fetcher) } function Avatar () { const { data, error } = useUser() if (error) return <Error /> if (!data) return <Spinner /> return <img src={data.avatar_url} /> } function App () { return <> <Avatar /> <Avatar /> <Avatar /> <Avatar /> <Avatar /> </> } 2. レンダリングの最適化 SWR はコンポーネントによって使用されている状態のみを更新する 更新は変数ごとに独立しているため、使用する変数を減らせば減らすほどレンダリングを抑えることが可能◎ 例1: data と error を使用していて fetch 中にエラーが出て再実行される場合 以下の例だとAppコンポーネントは4回レンダリングされます↓ function App () { const { data, error } = useSWR('/api', fetcher) console.log(data, error) return null } console.log(data, error) undefined undefined // => フェッチの開始 undefined Error // => フェッチの完了、エラーを取得 undefined Error // => フェッチ再試行の開始 Data undefined // => フェッチ再試行の完了、データを取得 // Appコンポーネントは4回レンダリングされる 例2: data のみ使用してfetch 中にエラーが出て再実行される場合 以下の例だとAppコンポーネントは2回のレンダリングで済む?よしなにやってくれていて最高! function App () { const { data } = useSWR('/api', fetcher) console.log(data) return null } console.log(data) undefined // => フェッチの開始 Data // => フェッチの完了 // Appコンポーネントがは2回レンダリングされる // 公式サイトではこれを「魔法が起きている」と表現されていたw 感想・まとめ SWRとstale-while-revalidateでキャッシュを出すタイミングが若干違っているのを知った時は困惑した(同じじゃないのかい!という困惑) Reduxで管理していたところをまるっとSWRが担ってくれているのは嬉しい!!!!! 調べてるとキャッシュの取り扱いはプログラミング上一番難しいと書かれていたり、キャッシュの設計は永遠の課題などと言われていたので、キャッシュ管理系ライブラリの存在は偉大! 参考 リモートのデータ取得のためのフックライブラリの SWR を使ってみる Stale-While-Revalidate ヘッダによるブラウザキャッシュの非同期更新 | blog.jxck.io Keeping things fresh with stale-while-revalidate
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nx入門】Reactで簡単にmonorepoの設定をする方法

ついに始まりましたね!!Qiita アドベントカレンダー 今年は1人で25日分の記事を書くチャレンジをしています、ちーずです。 業務でのフロントエンド開発を通して学んだことを毎日アウトプットしていこうと思います!! 本日のテーマは「Nx」です!! Nxとは Nxとは、React / node.js / Angularのアプリケーションをモノレポ(monorepo)で管理ができるの開発ツールセットです。 ReactのフレームワークであるGatsbyやNext.jsも使うこともできます。 (TypeScriptと相性の悪いVueやNuxt.jsはサポートしていない様子...悲しい) モノレポ(monorepo)とは モノレポは「モノリシックリポジトリ(monolithic repository)」 の略で、 単一のリポジトリで複数のアプリケーションやライブラリを管理することです。 GoogleやUberなど、サービスを複数運用している企業・組織においてよく利用されています。 モノレポのメリット 環境構築(lintやtsconfig)やビルドなどのプロセスを統一できる プロジェクトを跨いでコードを共有できるため、コードをDRY(Don’t Repeat Yourself:繰り返しを避けること)に保つことができる モノレポのデメリット リポジトリの肥大化 リポジトリで管理している全サービスのリリース、マージタイミングを考える必要がある 参考 ▼ 運用してわかった!モノレポに向いているプロジェクト Nxのここがすごい! セットアップが超簡単! もともと、モノレポの環境を構築するのには様々な設定が必要でした。 しかし、Nxでは1コマンドを実行するだけで簡単にモノレポ環境の構築ができます!! さらに、Jest、Cypress、Storybook、ESLint、TypeScript、Prettier、CSSフレームワークなどの開発に必要なツールも一緒にインストールすることも可能です◎ エディタの拡張機能「Nx Console」を使えば、GUIを用いてNxコマンドを生成することができます。 コマンドを覚えなくても感覚で使えるため、おすすめです。 参考 ライブラリとアプリケーションの依存関係が自動管理!ビジュアライズも!! NxはTypeScript の path aliasやnx.json の implicitDependenciesなどからコードを分析して、依存関係を解析します。 そのため、変更の影響を受ける可能性のあるプロジェクトだけを再ビルドと再テストをすることが可能です! また、その依存関係を1コマンドでビジュアライズすることもできます!! 参考 バージョン管理も楽ちん! Nxでは、nx migrateというコマンドを実行することで、依存パッケージも含めアップデートされます。 例えば、Next.jsのバージョンを最新のものにしたい場合、下記コマンドを実行すると、 Next.jsのバージョンだけではなく依存パッケージであるeslint-config-nextもアップデートされます。 yarn nx migrate @nrwl/next 参考 Nxをはじめてみる Nxの構造 Nxではこのようなディレクトリ設計になっています。 ? ワークスペース   └ ? apps - 実行可能なアプリケーション   └ ? libs - ライブラリ(共通の処理やUI、componentなど)   └ ? tools - コードベースで動作するスクリプト - ? workspace.json - ワークスペースの設定 - ? nx.json - プロジェクトの追加情報(依存関係の制御など) - ? tsconfig.base.json - グローバルなTypeScriptの設定 ワークスペースとアプリケーション作成(初期設定) 下記コマンドを実行します。 npx create-nx-workspace@latest すると、いくつか質問されるため1つずつ回答していきます。 まずはワークスペース名を設定します。 ? Workspace name (e.g., org name) › ワークスペースと共に何のアプリケーションを作成する作成します。 ? What to create in the new workspace … empty [an empty workspace with a layout that works best for building apps] npm [an empty workspace set up to publish npm packages (similar to and compatible with yarn workspaces)] react [a workspace with a single React application] angular [a workspace with a single Angular application] ❯ next.js [a workspace with a single Next.js application] gatsby [a workspace with a single Gatsby application] nest [a workspace with a single Nest application] express [a workspace with a single Express application] web components [a workspace with a single app built using web components] react-native [a workspace with a single React Native application] react-express [a workspace with a full stack application (React + Express)] angular-nest [a workspace with a full stack application (Angular + Nest) 作成するアプリケーションの名前を決めます。 ? Application name › Next.jsを選択したため、CSSフレームワークも一緒にインストールすることができます。 自らセットアップしなくても良いので楽ですね! ? Default stylesheet format … CSS SASS(.scss) [ http://sass-lang.com ] LESS [ http://lesscss.org ] Stylus(.styl) [ http://stylus-lang.com ] styled-components [ https://styled-components.com ] ❯ emotion [ https://emotion.sh ] styled-jsx [ https://www.npmjs.com/package/styled-jsx ] 全ての質問が完了したら、ワークスペースが生成されます。 アプリケーションを追加 / ライブラリを追加 nx generateコマンドを実行することで、アプリケーションやライブラリを追加することができます。 Nx Consoleを使うと簡単に設定ができるので、その方法で説明します。 1. Nx ConsoleのGENERATE & RUN TARGETからGenerateを選択 2. パッケージを選択 ダウンロードしてあるパッケージ一覧が標示されるので、 どのようなアプリケーション、ライブラリを追加したいのかを選択します。 もし該当するパッケージがない場合は、 ドキュメントまたは下記リポジトリを参考にインストールしてください。 3. optionの設定 ライブラリ名は何なのか、どの階層・ディレクトリに作成するかなど、細かいをぽちぽちと行います。 その設定からコマンドが自動で生成され、「Run」ボタンをクリックしたら実行できます! これで完成です! ぽちぽちするだけで設定が終わるので、本当に超簡単ですね!! 以上、「Nx入門」でした!! 明日はReactにおけるState管理に関してです! お楽しみに〜
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactとVueとAngular、今どれを選択するか

ReactとVueとAngular、今どれを選ぶかを考えてみたい。ここでは内製でサービスを開発しているケースを考える。 ※かなり私見です。 前提として: Reactがかなり有力ではある 現状、一番人気が高いのはReactである。「これを使えば間違いない」みたいな言い方もよくされている。では、実際はどうか。 ReactはTypeScriptと相性が良いことが一番重要なポイントだと思う。TypeScriptは、誤解を恐れずに言えばJavaのような感覚で書けるJavaScriptだ。2016年くらいにあるプロジェクトで導入した時、いろいろなところから集められたメンバーのほとんどはJavaの経験はあってもJavaScriptを書いたことがなく、教育の時間もなかった。それでも意味のあるコードが生産され、進捗が生まれていた(良いやり方だとは言い難いが、ハードな状況はどうしても起こるものである)。素のJavaScriptで書いていたら、そもそもどうコードを構築すべきかから学んでいく必要があり、なかなか進捗も生まれなかっただろうと考えている。 このTypeScriptとReactが組み合わされることで何が可能になるかというと、 「普段はJavaなどを中心に書いているバックエンドエンジニアがUIも作ることができ、一人でサービス全体を作れる」 ということだ。これがサービス作りの観点からもっとも革命的なポイントだと思う。1人でサービス全体の構築ができ、ドッグフーディングしてもらえる状況に持ち込める。これによってプロトタイプ作りも機能の改善もこれまでより遥かに高速にできる。 日本の情報系の大学や学校では、CやJavaで教育をしていることが多いので、静的型言語で品質良くコードを書く能力を持つエンジニアは多い。その技能を活用できる。 Vueはどうか Vueはすごく簡単だと言われることがある。この簡単さがサービスづくりの観点で現実的な需要とどう対応するかというと、じつはReact + TypeScriptと似ていて、「バックエンドエンジニアがUIも作ることができ、一人でサービス全体を作れる」だと思う。 しかしTypeScriptのサポートが弱いことが課題である。実際、静的型言語に慣れたバックエンドエンジニアにとっては、ちょっとAPIが簡単なことよりも「あそこでは型が効くがここでは効かない」といった事がないほうが使いやすいだろう。Vueは一般的な簡単さを志向しているものの、多分バックエンドエンジニアにとってはReact + TypeScriptよりすこし難しい。1人でサービス全体を構築する力はやや劣るといえる。 VueでJSXを使うこともできるが、そのくらいならReactを使うべきだと考える。依存関係を少なくし一般的な方法で使う方がメンテナンスは簡単になる。 Angularはどうか Angularは最初からTypeScriptに対応しているが、そもそも手に入る情報が少ないのでバックエンドも同時に担当する場合簡単とは言い難い。一人でサービス全体を立ち上げられるほどの簡単さには至っていないと思う。ただ、Ionicを使うときは考慮に入るだろう。 では、Reactなのか? そうだとも言い難い。理由を説明するため、まず私が2016年にフロントエンドの技術選定を行った当時の、各々のフレームワークの状況を見てみたい。 フレームワーク 状況 AngularJS 開発終了が決まっていた。 Angular (2) 当時βだった。 Vue 当時Version 1.xであったが、私の確認した限り公式のサンプルでもブラウザによってうまく動いていないものが多かった。 React 特に技術的な問題はなく、非常に人気が高まっていた。 この状況では消去法で考えてもReactだったが、Reactは選ばなかった。 なぜかというと、当時のReactのライセンスは「BSD + PATENTS」という特許条項の含まれたものだったからだ。このライセンスは2017年にネット上で話題になり、ReactのライセンスがMITライセンスに変わる結果となった。この事件の詳細はこの記事を参考。 → スタートアップはReactを使うべきではない (BSD + patentsライセンスを考慮して) — もし、いつか大企業に買収されたいと望むなら これは大企業が提供するOSSのリスクを示す例だと思う。もちろん、今のFacebookが急にReactのライセンスを戻すとは思わない。しかし、 React以外のフレームワークが全部衰退して、React一強の状況になったらどうか? 事業が萎んでいき、どこかに買収されることになったらどうか? メタバース事業が大成功し、Web技術がさほど重要でなくなったらどうか? ということは考える。他にも以下のようなことを考える。 サーバーサイドレンダリングでReactが完全に含まれない静的ページが生成できるとしたら、ライセンス的な問題は完全になくなるか? GoogleのAngularはライセンスについてはリスクは低そうだが、AngularJSの互換性を捨ててAngular2を始めた判断は賛否が分かれる。 TypeScriptはMicrosoftだが、トランスパイラなのでライセンスで問題になることはなさそう? Reactのライセンスが変更になったとしたら、おそらくその時点からのフォークが開発されるだろうが、それがどのくらいの品質になりえるか? Next.jsはSvelteで使えるようになるのだろうか? などなど… 結論 歯切れの良い結論は出ない。 現時点では、やはり開発効率の観点からReactが第一選択となる。そのなかでもNext.js + TypeScriptは最初に検討される。ただ、できるだけシンプルで軽量にできないかの検討も行うだろう。フレームワークなしで書けるプロジェクトであれば、それも積極的な選択肢になる。 Vueは、大企業の提供するOSSのリスクを考えれば、Vueを推し・使い・研究するのは重要だと考える。実験的な・個人的なプロジェクトには積極的に使いたい。 Angularは、Ionicを使うときは選択肢になる。 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React customHooks を利用してロジックとUIを切り離すまでをやってみた(3/n)

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 前回でロジック作成はできたので 今回は、UI部分となるコンポーネント作成をしていきます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成 ? 今回はコレ 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 UI(コンポーネント)部分のファイルを作成 components/ └── CreateUser.tsx hooks/ pages/ UI(コンポーネント)部分の実装 モジュールのimport components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' カスタムフックをコンポーネントで使えるようにする 前回作成したカスタムフックでreturnしている処理を全て分散代入します。 components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return () } コンポーネント部分の作成 components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return ( <> <p className="mb-3 font-bold">Custom Hook + useCallback + memo</p> <div className="mb-3 flex flex-col justify-center items-center"> <label>Text</label> <input className="px-3 py-2 border border-gray-300" type="text" value={text} onChange={handleTextChange} /> </div> <form className="flex flex-col justify-center items-center" onSubmit={handleSubmit} > <label>Username</label> <input className="mb-3 px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={username} onChange={usernameChange} /> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" type="submit" > Submit </button> </form> </> ) } まとめ 今回は、UI部分となるコンポーネント作成について書きました。 次回 ロジックとUIは切り離せたましたが、不要なレンダリングが走らないための工夫を説明するための準備を次回はします。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[WIP] React customHooks を利用してロジックとUIを切り離すまでをやってみた(3/n)

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 前回でロジック作成はできたので 今回は、UI部分となるコンポーネント作成をしていきます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成 ? 今回はコレ 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 UI(コンポーネント)部分のファイルを作成 components/ └── CreateUser.tsx hooks/ pages/ UI(コンポーネント)部分の実装 モジュールのimport components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' カスタムフックをコンポーネントで使えるようにする 前回作成したカスタムフックでreturnしている処理を全て分散代入します。 components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return () } コンポーネント部分の作成 components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return ( <> <div className="mb-3 flex flex-col justify-center items-center"> <label>Text</label> <input className="px-3 py-2 border border-gray-300" type="text" value={text} onChange={handleTextChange} /> </div> <form className="flex flex-col justify-center items-center" onSubmit={handleSubmit} > <label>Username</label> <input className="mb-3 px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={username} onChange={usernameChange} /> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" type="submit" > Submit </button> </form> </> ) } まとめ 今回は、UI部分となるコンポーネント作成について書きました。 次回 ロジックとUIは切り離せたましたが、不要なレンダリングが走らないための工夫を説明するための準備を次回はします。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React customHooks を利用してロジックとUIを切り離すまでをやってみた(3/5)〜コンポーネント(UI)作成〜

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 前回でロジック作成はできたので 今回は、UI部分となるコンポーネント作成をしていきます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成? 今回はコレ 不要なレンダリングが走らないための工夫(準備編) 不要なレンダリングが走らないための工夫(検証 & 改善編) 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 UI(コンポーネント)部分のファイルを作成 components/ └── CreateUser.tsx hooks/ pages/ UI(コンポーネント)部分の実装 モジュールのimport components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' カスタムフックをコンポーネントで使えるようにする 前回作成したカスタムフックでreturnしている処理を全て分散代入します。 components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return () } コンポーネント部分の作成 components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return ( <> <div className="mb-3 flex flex-col justify-center items-center"> <label>Text</label> <input className="px-3 py-2 border border-gray-300" type="text" value={text} onChange={handleTextChange} /> </div> <form className="flex flex-col justify-center items-center" onSubmit={handleSubmit} > <label>Username</label> <input className="mb-3 px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={username} onChange={usernameChange} /> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" type="submit" > Submit </button> </form> </> ) } まとめ 今回は、UI部分となるコンポーネント作成について書きました。 次回 ロジックとUIは切り離せたましたが、不要なレンダリングが走らないための工夫を説明するための準備を次回はします。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React customHooks を利用してロジックとUIを切り離すまでをやってみた(2/n)

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 今回は、Create処理をcustomHooksとして切り出すための実装について書きます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 ? 今回はコレ コンポーネント(UI)作成 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 customHooks作成に必要なファイルを作成 components/ hooks/ └── useCreateForm.ts pages/ customHooks作成 モジュールのimport hooks/useCreateForms.ts import { useState, useCallback, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' // apolloを用いた通信処理 import { CREATE_USER } from '../queries/queries' // クエリコマンド import { CreateUserMutation } from '../types/generated/graphql' // クエリコマンドに対応したデータ型 カスタムフックの定義 hooks/useCreateForms.ts export const useCreateForm = () => {} 2つのuseStateの定義 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') } create処理を実装 前作ったやつをそのまま持ってきただけ hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } これでユーザーの新規作成処理が実装できました。 input要素を入力したときのイベント処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) }, []) 引数はイベント要素を受け取るようにして、setText()だけする処理にしています。 ユーザー名を変える処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) }, []) const usernameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) }, []) submitしたときの処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) }, []) const usernameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) }, []) const handleSubmit = useCallback( async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') }, [username] ) 関数handleSubmitが実行されたら 非同期で、関数insert_users_oneが実行されてユーザー情報を{}オブジェクトの中に入れて送っています。 非同期処理が成功・失敗にかかわらず終わったら、ユーザーネームを空文字をsetStateしています(初期化)。 最終的な実装結果 クリックすると実際のコードが見れます import { useState, useCallback, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' import { CREATE_USER } from '../queries/queries' import { CreateUserMutation } from '../types/generated/graphql' export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) const handleTextChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) }, []) const usernameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) }, []) const printMsg = useCallback(() => { console.log('Hello') }, []) const handleSubmit = useCallback( async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') }, [username] ) return { text, handleSubmit, username, usernameChange, printMsg, handleTextChange, } } 最終的に作った関数をreturnしたり、検証用の関数なども入れてます。 まとめ 今回は、Create処理をcustomHooksとして切り出すための実装について書きました。 次回 customHooksを利用してロジックとUIを切り離すまでのうち、ロジック作成はできたので、UI部分となるコンポーネント作成をしていきます。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[WIP] React customHooks を利用してロジックとUIを切り離すまでをやってみた(2/n)

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 今回は、Create処理をcustomHooksとして切り出すための実装について書きます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 ? 今回はコレ コンポーネント(UI)作成 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 customHooks作成に必要なファイルを作成 components/ hooks/ └── useCreateForm.ts pages/ customHooks作成 モジュールのimport hooks/useCreateForms.ts import { useState, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' // apolloを用いた通信処理 import { CREATE_USER } from '../queries/queries' // クエリコマンド import { CreateUserMutation } from '../types/generated/graphql' // クエリコマンドに対応したデータ型 カスタムフックの定義 hooks/useCreateForms.ts export const useCreateForm = () => {} 2つのuseStateの定義 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') } create処理を実装 前作ったやつをそのまま持ってきただけ hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } これでユーザーの新規作成処理が実装できました。 input要素を入力したときのイベント処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } 引数はイベント要素を受け取るようにして、setText()だけする処理にしています。 ユーザー名を変える処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } const usernameChange = (e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) } submitしたときの処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } const usernameChange = (e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) } const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') } 関数handleSubmitが実行されたら 非同期で、関数insert_users_oneが実行されてユーザー情報を{}オブジェクトの中に入れて送っています。 非同期処理が成功・失敗にかかわらず終わったら、ユーザーネームを空文字をsetStateしています(初期化)。 最終的な実装結果 クリックすると実際のコードが見れます import { useState, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' import { CREATE_USER } from '../queries/queries' import { CreateUserMutation } from '../types/generated/graphql' export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } const usernameChange = (e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) } const printMsg = () => { console.log('Hello') } const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') } return { text, handleSubmit, username, usernameChange, printMsg, handleTextChange, } } 最終的に作った関数をreturnしたり、検証用の関数なども入れてます。 まとめ 今回は、Create処理をcustomHooksとして切り出すための実装について書きました。 次回 customHooksを利用してロジックとUIを切り離すまでのうち、ロジック作成はできたので、UI部分となるコンポーネント作成をしていきます。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React customHooks を利用してロジックとUIを切り離すまでをやってみた(2/5)〜ロジックの作成〜

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 今回は、Create処理をcustomHooksとして切り出すための実装について書きます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成? 今回はコレ コンポーネント(UI)作成 不要なレンダリングが走らないための工夫(準備編) 不要なレンダリングが走らないための工夫(検証 & 改善編) 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 customHooks作成に必要なファイルを作成 components/ hooks/ └── useCreateForm.ts pages/ customHooks作成 モジュールのimport hooks/useCreateForms.ts import { useState, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' // apolloを用いた通信処理 import { CREATE_USER } from '../queries/queries' // クエリコマンド import { CreateUserMutation } from '../types/generated/graphql' // クエリコマンドに対応したデータ型 カスタムフックの定義 hooks/useCreateForms.ts export const useCreateForm = () => {} 2つのuseStateの定義 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') } create処理を実装 前作ったやつをそのまま持ってきただけ hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } これでユーザーの新規作成処理が実装できました。 input要素を入力したときのイベント処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } 引数はイベント要素を受け取るようにして、setText()だけする処理にしています。 ユーザー名を変える処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } const usernameChange = (e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) } submitしたときの処理の実装 hooks/useCreateForms.ts export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) } const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } const usernameChange = (e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) } const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') } 関数handleSubmitが実行されたら 非同期で、関数insert_users_oneが実行されてユーザー情報を{}オブジェクトの中に入れて送っています。 非同期処理が成功・失敗にかかわらず終わったら、ユーザーネームを空文字をsetStateしています(初期化)。 最終的な実装結果 クリックすると実際のコードが見れます import { useState, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' import { CREATE_USER } from '../queries/queries' import { CreateUserMutation } from '../types/generated/graphql' export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } const usernameChange = (e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) } const printMsg = () => { console.log('Hello') } const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') } return { text, handleSubmit, username, usernameChange, printMsg, handleTextChange, } } 最終的に作った関数をreturnしたり、検証用の関数なども入れてます。 まとめ 今回は、Create処理をcustomHooksとして切り出すための実装について書きました。 次回 customHooksを利用してロジックとUIを切り離すまでのうち、ロジック作成はできたので、UI部分となるコンポーネント作成をしていきます。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React Three Fiber】GodRaysエフェクトの実装

概要 React Three Fiberで、GodRaysエフェクトの実装方法をまとめました。 https://nemutas.github.io/r3f-godrays-effect/ GodRaysとは、光が差し込むようなエフェクトのことです。 ドキュメント・サンプル ※ ただし、公式サンプルはshaderを独自に作成しているため、実装の難易度が高いです。 3Dモデル 3Dモデルは、Sketchfabから以下のものをお借りしました。 ※ 実装では、Blenderでglbファイルに変換したものを使用しています。 実装 コンポーネント毎に解説します。 FallenAngel.tsx Canvasを作成しているコンポーネントです。コンポーネント名は、使用する3Dモデルに因んでいます。 FallenAngel.tsx import React, { Suspense, VFC } from 'react'; import { OrbitControls, Stats } from '@react-three/drei'; import { Canvas } from '@react-three/fiber'; import { Effects } from './Effects'; import { Lights } from './Lights'; import { Model } from './Model'; export const FallenAngel: VFC = () => { return ( <Canvas camera={{ position: [0, -1.5, 3], fov: 50, aspect: window.innerWidth / window.innerHeight, near: 0.1, far: 2000 }} dpr={window.devicePixelRatio} shadows> {/* canvas color */} <color attach="background" args={['#000']} /> {/* fps */} <Stats /> {/* camera controller */} <OrbitControls /> {/* lights */} <Lights /> {/* objects */} <Suspense fallback={null}> <Model position={[0, 0.3, 0]} /> </Suspense> {/* effects */} <Effects /> {/* helper */} {/* <axesHelper /> */} </Canvas> ) } GodRaysを含めた、Postprocessingを使用する際に気をつけるのは、Canvasの背景色です。 エフェクト自体がCanvas全体にかかり、背景色とブレンドして描画します。GodRaysのブレンドモードのデフォルト値はScreenなので、より明確に効果がわかる#000を背景色として設定しています。 <color attach="background" args={['#000']} /> Model.tsx 3Dモデルを読み込むためのコンポーネントです。 1から実装しているわけではなく、雛形のコードを生成するツールを使用して、それを書き換えています。 ツールについては、以下を参照してください。 Model.tsx /* Auto-generated by: https://github.com/pmndrs/gltfjsx */ import { useControls } from 'leva'; import React, { useRef, VFC } from 'react'; import * as THREE from 'three'; import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'; import { useGLTF } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; type GLTFResult = GLTF & { nodes: { Mesh_0001: THREE.Mesh } materials: { ['Low_chapeau1.001']: THREE.MeshStandardMaterial } } const ModelPath = process.env.PUBLIC_URL + '/assets/fallen_angel.glb' export const Model: VFC<JSX.IntrinsicElements['group']> = props => { // add controller const datas = useControls('model', { rotate: false }) const group = useRef<THREE.Group>() const { nodes, materials } = (useGLTF(ModelPath) as unknown) as GLTFResult materials['Low_chapeau1.001'].side = THREE.FrontSide useFrame(() => { if (datas.rotate) { group.current!.rotation.y += 0.002 } }) return ( <group ref={group} {...props} dispose={null}> <mesh castShadow receiveShadow geometry={nodes.Mesh_0001.geometry} material={materials['Low_chapeau1.001']} /> </group> ) } useGLTF.preload(ModelPath) 今回作成したアプリケーションは、Github Pagesにアップロードしています。 この場合、publicフォルダにある3Dモデル(fallen_angel.glb)を参照するには、process.env.PUBLIC_URLを追加する必要があります。 const ModelPath = process.env.PUBLIC_URL + '/assets/fallen_angel.glb' 読み込んだ3Dモデルに影を適用している場合(receiveShadow)、モデルの表面にジャギーが発生することがあります。 これは、materialがside=DoubleSideになっていることに起因しているようです。この場合、FrontSideを指定することで正常に表示されます。 materials['Low_chapeau1.001'].side = THREE.FrontSide Lights.tsx ライトを設定するコンポーネントです。 Lights.tsx import { useControls } from 'leva'; import React, { useEffect, useRef, VFC } from 'react'; import * as THREE from 'three'; import { useThree } from '@react-three/fiber'; export const Lights: VFC = () => { return ( <> <ambientLight intensity={0.1} /> <PointLight position={[0, 3, -5]} /> </> ) } type PointLightProps = { position: [number, number, number] } const PointLight: VFC<PointLightProps> = ({ position }) => { // add controller const datas = useController() const meshRef = useRef<THREE.Mesh>() const { scene } = useThree() useEffect(() => { if (!scene.userData.refs) scene.userData.refs = {} scene.userData.refs.lightMesh = meshRef }, [scene.userData]) useEffect(() => { meshRef.current!.lookAt(0, 0, 0) }, []) return ( <mesh ref={meshRef} position={position}> <circleGeometry args={[datas.size, 64]} /> <meshBasicMaterial color={datas.color} side={THREE.DoubleSide} /> <pointLight color={datas.color} intensity={1} /> </mesh> ) } const useController = () => { const datas = useControls('light', { size: { value: 4.5, min: 0.2, max: 10, step: 0.1 }, color: '#525252' }) return datas } GodRaysは、実際にはライトではなくメッシュに適用します。 ただし、ライト(pointLight)の位置とGodRaysをかけるメッシュの位置を一致させておかないと、描画上の矛盾が発生してしまうため、Lightsコンポーネントでメッシュも作成しています。 <mesh ref={meshRef} position={position}> <circleGeometry args={[datas.size, 64]} /> <meshBasicMaterial color={datas.color} side={THREE.DoubleSide} /> <pointLight color={datas.color} intensity={1} /> </mesh> 今回は、メッシュとGodRaysを実装しているコンポーネントを分けているので、コンポーネント間でメッシュデータを渡す必要があります。 渡し方にはいくつか方法があります。 1)sceneにuserDataを追加して渡す 2)メッシュに名前をつけてsceneから取得する 3)recoilなどのglobal stateを使用して渡す 実装例では、メッシュ情報をsceneにuserDataとして追加して、Effectsコンポーネントで参照するようにしています。 useEffect(() => { if (!scene.userData.refs) scene.userData.refs = {} scene.userData.refs.lightMesh = meshRef }, [scene.userData]) Effects.tsx GodRaysを追加しているコンポーネントです。 import { useControls } from 'leva'; import React, { useEffect, useState, VFC } from 'react'; import THREE from 'three'; import { useThree } from '@react-three/fiber'; import { EffectComposer, GodRays } from '@react-three/postprocessing'; export const Effects: VFC = () => { // add controller const datas = useController() const [lightMesh, setLightMesh] = useState< React.MutableRefObject<THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>> >() const { scene } = useThree() useEffect(() => { if (scene.userData.refs && scene.userData.refs.lightMesh) { const lightMeshRef = scene.userData.refs.lightMesh setLightMesh(lightMeshRef) } }, [scene.userData.refs]) return ( <EffectComposer> <>{lightMesh && datas.enabled && <GodRays sun={lightMesh.current!} {...datas} />}</> </EffectComposer> ) } // ======================================================== const useController = () => { const datas = useControls('godray', { enabled: true, samples: { value: 100, min: 10, max: 200, step: 10 }, density: { value: 0.96, min: 0, max: 1, step: 0.01 }, decay: { value: 0.98, min: 0, max: 1, step: 0.01 }, weight: { value: 0.3, min: 0, max: 1, step: 0.01 }, exposure: { value: 1, min: 0, max: 1, step: 0.01 }, blur: { value: 0, min: 0, max: 1, step: 0.01 } }) return datas } GodRaysを実装するには、引数にsunとして適用させるメッシュを渡す必要があります。 メッシュは、Lightsコンポーネントで実装しuserDataとしてsceneに格納しているので、sceneから取り出して使用します。Reactの仕様上、コンポーネントのレンダリングが非同期なので、useStateを使用してuserDataがセットされたタイミングでGodRaysが描画されるようにします。 const [lightMesh, setLightMesh] = useState< React.MutableRefObject<THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>> >() const { scene } = useThree() useEffect(() => { if (scene.userData.refs && scene.userData.refs.lightMesh) { const lightMeshRef = scene.userData.refs.lightMesh setLightMesh(lightMeshRef) } }, [scene.userData.refs]) return ( <EffectComposer> <>{lightMesh && datas.enabled && <GodRays sun={lightMesh.current!} {...datas} />}</> </EffectComposer> ) GodRaysがとる他の引数は、コントローラーで設定できるようにしています。 この値の意味は、実際にサンプルをいじって確認していただくのが早いと思います。 特に、メッシュの色の明度によっても見え方が全然違く、これらの値をうまく調整する必要があります。 公式ドキュメントの説明を載せておきます。 https://docs.pmnd.rs/react-postprocessing/effects/god-rays#props NAME DESCRIPTION samples ピクセルあたりのサンプル数 density 光線の密度 decay 照明減衰係数 weight 光線の重み係数 exposure 一定の減衰係数 blur 差し込む光のぼかし具合 リポジトリ まとめ 個人的に、Postprocessingは少し難しいです。 ですが、使いこなせれば表現力をぐっと高めることができると思うので、頑張って習得していきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React customHooks を利用してロジックとUIを切り離すまでをやってみた(1/n)

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 今回は、このシリーズの方針について書きます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 ? 今回はコレ customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 このシリーズの方針 Next.js(apollo)からHasuraに対してCRUD操作(クエリやミューテーション)をできるようにしました。 表側は特に問題ないかもですが コードを見てもらうと課題点もあります。 クリックすると実際のコードが見れます import { VFC, useState, FormEvent } from 'react' import { useQuery, useMutation } from '@apollo/client' import { GET_USERS, CREATE_USER, DELETE_USER, UPDATE_USER, } from '../queries/queries' import { GetUsersQuery, CreateUserMutation, DeleteUserMutation, UpdateUserMutation, } from '../types/generated/graphql' import { Layout } from '../components/Layout' import { UserItem } from '../components/UserItem' import { BreadcrumbJsonLd } from 'next-seo' const HasuraCRUD: VFC = () => { const [editedUser, setEditedUser] = useState({ id: '', name: '' }) const { data, error } = useQuery<GetUsersQuery>(GET_USERS, { fetchPolicy: 'cache-and-network', }) const [update_users_by_pk] = useMutation<UpdateUserMutation>(UPDATE_USER) const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) const [delete_users_by_pk] = useMutation<DeleteUserMutation>(DELETE_USER, { update(cache, { data: { delete_users_by_pk } }) { cache.modify({ fields: { users(existingUsers, { readField }) { return existingUsers.filter( (user) => delete_users_by_pk.id !== readField('id', user) ) }, }, }) }, }) const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() if (editedUser.id) { try { await update_users_by_pk({ variables: { id: editedUser.id, name: editedUser.name, }, }) } catch (err) { alert(err.message) } setEditedUser({ id: '', name: '' }) } else { try { await insert_users_one({ variables: { name: editedUser.name, }, }) } catch (err) { alert(err.message) } setEditedUser({ id: '', name: '' }) } } if (error) return <Layout title="Hasura CRUD">Error: {error.message}</Layout> return ( <Layout title="Hasura CRUD"> <BreadcrumbJsonLd itemListElements={[ { position: 1, name: 'Books', item: 'https://example.com/books', }, { position: 2, name: 'Authors', item: 'https://example.com/books/authors', }, { position: 3, name: 'Ann Leckie', item: 'https://example.com/books/authors/annleckie', }, { position: 4, name: 'Ancillary Justice', item: 'https://example.com/books/authors/ancillaryjustice', }, ]} /> <p className="mb-3 font-bold">Hasura CRUD</p> <form className="flex flex-col justify-center items-center" onSubmit={handleSubmit} > <input className="px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={editedUser.name} onChange={(e) => setEditedUser({ ...editedUser, name: e.target.value }) } /> <button disabled={!editedUser.name} className="disabled:opacity-40 my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" data-testid="new" type="submit" > {editedUser.id ? 'Update' : 'Create'} </button> </form> {data?.users.map((user) => { return ( <UserItem key={user.id} user={user} setEditedUser={setEditedUser} delete_users_by_pk={delete_users_by_pk} /> ) })} </Layout> ) } export default HasuraCRUD 上記のコードを見ると、以下が混在している状況です。 ロジック useQueryやuseMutationなどのapolloを使った通信処理 submitした際の処理(関数handleSubmit) UI(コンポーネント) これを、React の customHooksを利用してロジックとUIを切り離すことをこのシリーズではやっていきます。 また、コンポーネントも分離する関係で不要なレンダリングや処理が走るのを防止するためにmemoやuseCallbackを用いたパフォーマンス改善も実装していきます。 まとめ 今回は、このシリーズの方針について書きました。 次回 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[WIP] React customHooks を利用してロジックとUIを切り離すまでをやってみた(1/n)

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 今回は、このシリーズの方針について書きます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 ? 今回はコレ customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 このシリーズの方針 Next.js(apollo)からHasuraに対してCRUD操作(クエリやミューテーション)をできるようにしました。 表側は特に問題ないかもですが コードを見てもらうと課題点もあります。 クリックすると実際のコードが見れます import { VFC, useState, FormEvent } from 'react' import { useQuery, useMutation } from '@apollo/client' import { GET_USERS, CREATE_USER, DELETE_USER, UPDATE_USER, } from '../queries/queries' import { GetUsersQuery, CreateUserMutation, DeleteUserMutation, UpdateUserMutation, } from '../types/generated/graphql' import { Layout } from '../components/Layout' import { UserItem } from '../components/UserItem' import { BreadcrumbJsonLd } from 'next-seo' const HasuraCRUD: VFC = () => { const [editedUser, setEditedUser] = useState({ id: '', name: '' }) const { data, error } = useQuery<GetUsersQuery>(GET_USERS, { fetchPolicy: 'cache-and-network', }) const [update_users_by_pk] = useMutation<UpdateUserMutation>(UPDATE_USER) const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) const [delete_users_by_pk] = useMutation<DeleteUserMutation>(DELETE_USER, { update(cache, { data: { delete_users_by_pk } }) { cache.modify({ fields: { users(existingUsers, { readField }) { return existingUsers.filter( (user) => delete_users_by_pk.id !== readField('id', user) ) }, }, }) }, }) const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() if (editedUser.id) { try { await update_users_by_pk({ variables: { id: editedUser.id, name: editedUser.name, }, }) } catch (err) { alert(err.message) } setEditedUser({ id: '', name: '' }) } else { try { await insert_users_one({ variables: { name: editedUser.name, }, }) } catch (err) { alert(err.message) } setEditedUser({ id: '', name: '' }) } } if (error) return <Layout title="Hasura CRUD">Error: {error.message}</Layout> return ( <Layout title="Hasura CRUD"> <BreadcrumbJsonLd itemListElements={[ { position: 1, name: 'Books', item: 'https://example.com/books', }, { position: 2, name: 'Authors', item: 'https://example.com/books/authors', }, { position: 3, name: 'Ann Leckie', item: 'https://example.com/books/authors/annleckie', }, { position: 4, name: 'Ancillary Justice', item: 'https://example.com/books/authors/ancillaryjustice', }, ]} /> <p className="mb-3 font-bold">Hasura CRUD</p> <form className="flex flex-col justify-center items-center" onSubmit={handleSubmit} > <input className="px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={editedUser.name} onChange={(e) => setEditedUser({ ...editedUser, name: e.target.value }) } /> <button disabled={!editedUser.name} className="disabled:opacity-40 my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" data-testid="new" type="submit" > {editedUser.id ? 'Update' : 'Create'} </button> </form> {data?.users.map((user) => { return ( <UserItem key={user.id} user={user} setEditedUser={setEditedUser} delete_users_by_pk={delete_users_by_pk} /> ) })} </Layout> ) } export default HasuraCRUD 上記のコードを見ると、以下が混在している状況です。 ロジック useQueryやuseMutationなどのapolloを使った通信処理 submitした際の処理(関数handleSubmit) UI(コンポーネント) これを、React の customHooksを利用してロジックとUIを切り離すことをこのシリーズではやっていきます。 また、コンポーネントも分離する関係で不要なレンダリングや処理が走るのを防止するためにmemoやuseCallbackを用いたパフォーマンス改善も実装していきます。 まとめ 今回は、このシリーズの方針について書きました。 次回 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む