20210727のReactに関する記事は9件です。

React+Django+AWSでペットの成長をサポートするアプリを開発したのでTipsをまとめた

biby diary https://biby-diary.studio.site/ 本日、ペットの成長を記録できるアプリ「biby diary」をリリースしました。 biby diaryは日々のペットの成長を記録できるほか、家族間でのペットの情報を共有することができます。 さらにbiby検索から動物病院を検索することもできるので、緊急時などにお近くの住所から迅速に動物病院を見つけることもできます。 スマホ推奨で、PWAにも対応しているのでブラウザからホーム画面に追加することで快適にご利用いただけます。 追加する際はこちらのリンクから追加してください。 https://diary.biby.live アーキテクチャーにはDjango、React、AWSを採用しており、これらの技術で実務にあたられている方や採用を検討している方の参考になればと思いまとめました。 もし紹介する内容以外でベストプラクティスがあれば、ぜひコメント欄へ共有いただきたいです。 目次 アーキテクチャ詳細 ReactのTips DjangoのTips ngrok 今後の計画 アーキテクチャ詳細 全体のシステムはこのような図になっています。 biby diaryはReact+TypeScriptとDjangoで開発しました。サーバーはAWSのものを使用しており、フロントエンドにはamplify、バックエンドにはEC2を使用しています。 amplifyを使うことでURLのSSL化やデプロイの設定などを簡単に行うことができます。 ドメインはお名前.comで取得しているのですが、ネームサーバーをAWSに向けておけばCNAMEなどの設定も自動で行ってくれるので非常に楽でした。 バックエンドにEC2を使うことで、CodeDeployとCodePipelineを使って容易に自動デプロイの設定ができます。 フロントエンドとバックエンドを分けたのは、後々スマホアプリ化も検討しているため、あえて分離しています。 現在はWebブラウザだけですが、PWAにも対応しているのでホーム画面にアプリを追加していただければアプリのような操作感でお使いいただけます。 今回初めてAWSを使用しましたが、ほかのクラウドサービスと比べて非常に使いやすい印象でした。 さすがクラウドサービス市場のトップです。ちなみに2位はAzureです。 G○Pだとなぜそうなる?みたいことになりがちだったので、これからはAWSを推していこうと思います。 ReactのTips Reactは以前から使用した経験があったのと、Adobeが提供しているReact Spectrumを使ってみたかったこともあり採用しました。 React Spectrumについては本記事では詳しく説明しないので、別の機会にまとめようと思っています。 Reactでクッキーを操作する方法 biby diaryではログインした時にAPIにリクエストを送るためのトークンを発行しているのですが、そのトークンを持っておくためにクッキーを使用しています。 Reactにはreact-cookiesというクッキーの操作が簡単にできるパッケージがあります。 https://github.com/reactivestack/cookies/tree/master/packages/react-cookie 利用例 まずはアプリ全体をCookiesProviderラップします。 import React from 'react'; import App from './App'; import { CookiesProvider } from 'react-cookie'; export default function Root() { return ( <CookiesProvider> <App /> </CookiesProvider> ); } アプリを登録する場合は下記のようになります。 aaa import React from 'react'; import { useCookies } from 'react-cookie'; import NameForm from './NameForm'; function App() { const [cookies, setCookie] = useCookies(['name']); function onChange(newName) { setCookie('name', newName, { path: '/' }); } return ( <div> <NameForm name={cookies.name} onChange={onChange} /> {cookies.name && <h1>Hello {cookies.name}!</h1>} </div> ); } export default App; useCookiesを使用したい箇所でインポートし、const [cookies, setCookie] = useCookies(['name']);でクッキーを登録できるようにします。 実際の登録は、setCookie('name', newName, { path: '/' });となっており、pathオプションを指定することでクッキーの保存対象にしたいページを選択できます。/を指定した場合はアプリのページ全てに対してクッキーを保存することになります。 ログアウト時などにクッキーを削除したい場合は、removeCookieを使用します。 removeCookieにもpathオプションを指定することができ、removeCookie('name', { path: '/' })としておくことでページ全体に対して選択したクッキーを削除できます。 あえて{ path: '/' }を指定しておくことをおすすめします。 環境変数の設定 Reactの構築にはCreate React Appを用いており、Create React Appにはデフォルトで環境変数を供給する機構が備わっています。 使い方は簡単でプロジェクト直下に.envファイルを作成し、REACT_APP_をプレフィックスにした変数を入れておくことでReactアプリ内で呼び出すことができます。 呼び出す時はprocess.env.REACT_APP_[変数名]で変数を取得できます。 利用例 REACT_APP_TEST="test" 環境変数を呼び出す時はこのようになります。 const test = process.env.REACT_APP_TEST; amplifyでの環境変数設定 .envファイルは機密情報を含める場合もあるため.gitignoreに入れておくことが望ましく、デプロイ時に.envを生成する必要があります。 amplifyでは管理画面のサイドバーにある環境変数から設定を行い、同じくサイドバーのビルドの設定から.envファイルの生成コマンドを追記します。 環境変数設定画面を開くとこのような画面が出てきます。 変数と値をそれぞれ入力します。 続いてビルドの設定を開くと、下記のようなビルド設定が表示されていると思います。 amplify.yml version: 1 frontend: phases: preBuild: commands: - yarn install build: commands: - yarn run build artifacts: baseDirectory: build files: - '**/*' cache: paths: - node_modules/**/* .envの生成はbuild>commandsに- echo "REACT_APP_TEST=$REACT_APP_BACKEND_TEST" >> .envを追記します。 同じ画面からymlファイルの編集ができるので、そのまま追記できます。 画像アップロード 画像アップロードに手こずってしまったので、同じような境遇に会った方、会われるであろう方に向けてTipsを紹介しておきます。 基本的にAPIリクエスト時にデータを渡すときは、JSON形式にしているのですが画像をアップロードする時にどうしてもJSONではリクエストに失敗するということがありました。 調べるとJSON形式でもファイル送信ができるみたいな情報が出てきたのですが、実現できなかったのでファイルアップロードによく使われるmultipart/form-data形式を使うことにしました。 最終的にmultipart/form-dataで無事に画像ファイルは送信できるようになったのですが、もう一つ躓いたポイントがあり、リクエストヘッダのcontent-typeに明示的にmultipart/form-dataを指定するとリクエストに失敗する現象が発生しました。 理由はいまだにわからないので、もし分かる方いたら教えてください。 DjangoのTips DjangoにはDjango Rest FrameworkというREST APIを実装できるパッケージがあるのですが、これがかなり使いやすく容易にAPIを開発できるのでDjangoを採用しました。 デフォルトで管理画面が付いている点も理由の1つです。 CORS問題 API開発でやっかいなのが、CORS問題です。CORSは同一生成元ポリシーと呼ばれるブラウザのセキュリティ仕様で、あるサーバーにWebからリクエストを送る時に同じドメインであればエラーは起きないのですが、違うドメインからのリクエストは脆弱性から守るために拒否されます。 このサイトがわかりやすくCORS問題について説明しているので、気になる方はご参照ください。 https://coliss.com/articles/build-websites/operation/work/cs-visualized-cors.html CORS問題を解消するにはバックエンド側でCORSの制御をする必要があります。 corsheadersというパッケージを使うことで、CORS問題を片付けることができます。 https://github.com/adamchainz/django-cors-headers 環境変数 Djangoは環境を立ち上げた時にシークレットキーが作られます。このシークレットキーはユニークIDの生成などに使用されるので、gitで管理するようなことはしたくありません。 そのため、環境変数で持っておくことが望ましいです。 Djangoには、django-environという環境変数を設定するためのパッケージがあります。 https://github.com/joke2k/django-environ このパッケージを使うことで容易に環境変数の設定ができます。 使い方 プロジェクト直下に.envファイルを作成し変数を追記していきます。 具体的にはこのような形で追記します。 代入する値はダブルクオートなどで囲む必要はないのでご注意ください。 DEBUG=on SECRET_KEY=your-secret-key 呼び出す時はこのようになります。 settings.py import environ env = environ.Env( # set casting, default value DEBUG=(bool, False) ) # reading .env file environ.Env.read_env() # False if not in os.environ DEBUG = env('DEBUG') # Raises django's ImproperlyConfigured exception if SECRET_KEY not in os.environ SECRET_KEY = env('SECRET_KEY') ngrok アプリを開発しているとスマホでも実際に挙動を確認したいということがあると思うのですが、フロントエンドとバックエンドを分離していると、ローカル環境ではAPIのドメインがlocalhostなため確認できません。 フロントにはアクセスできるけどバックエンドにはつながらないといったことになると思います。 このときにおすすめなのがngrokという開発ツールで、ngrokを使用することで一時的に作られるドメインとローカルのドメインを結びつけることができ、他のデバイスからアクセスすることができます。 https://ngrok.com/ 開発途中のアプリをngrokを使ってクライアントに見てもらうこともできるので、実務でも大活躍します。 会員登録なしでも使用することができるのですが、その場合一時的に発行されるドメインの有効期限が決まっています。 有効期限なしで使用したい場合は、無料プランの登録もできるので会員登録しておくことをおすすめします。 起動はターミナルを開いてngrok http ポート番号と入力するだけなので簡単にアプリを外部に向けて公開できます。 今後の計画 biby diaryはまだまだ未完成なので、継続的に開発をしていきます。 今は無料プランしかないので近々有料プランを追加する予定しているほか、bibyユーザーがペットへの寄付を外部に募ることができる機能も考えています。 追加して欲しい機能も絶賛募集中なので、ぜひご連絡ください。本記事のコメント欄もしくは、こちらのお問い合わせフォームからご要望いただくこともできます。 https://forms.gle/EJdXYLNWbeLfCccm9 形だけですがロードマップを公開しているので、気になる方はぜひご覧ください。 https://uichi.notion.site/5bead4691e344d6b946a09f9aa8c7d1f?v=3fccb1b559ef4c27ab0872db33a95690!%5BFireShot
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reac+Django+AWSでペットの成長をサポートするアプリを開発したのでTipsをまとめた

biby diary https://biby-diary.studio.site/ 本日、ペットの成長を記録できるアプリ「biby diary」をリリースしました。 biby diaryは日々のペットの成長を記録できるほか、家族間でのペットの情報を共有することができます。 さらにbiby検索から動物病院を検索することもできるので、緊急時などにお近くの住所から迅速に動物病院を見つけることもできます。 スマホ推奨で、PWAにも対応しているのでブラウザからホーム画面に追加することで快適にご利用いただけます。 追加する際はこちらのリンクから追加してください。 https://diary.biby.live アーキテクチャーにはDjango、React、AWSを採用しており、これらの技術で実務にあたられている方や採用を検討している方の参考になればと思いまとめました。 もし紹介する内容以外でベストプラクティスがあれば、ぜひコメント欄へ共有いただきたいです。 目次 アーキテクチャ詳細 ReactのTips DjangoのTips ngrok 今後の計画 アーキテクチャ詳細 全体のシステムはこのような図になっています。 biby diaryはReact+TypeScriptとDjangoで開発しました。サーバーはAWSのものを使用しており、フロントエンドにはamplify、バックエンドにはEC2を使用しています。 amplifyを使うことでURLのSSL化やデプロイの設定などを簡単に行うことができます。 ドメインはお名前.comで取得しているのですが、ネームサーバーをAWSに向けておけばCNAMEなどの設定も自動で行ってくれるので非常に楽でした。 バックエンドにEC2を使うことで、CodeDeployとCodePipelineを使って容易に自動デプロイの設定ができます。 フロントエンドとバックエンドを分けたのは、後々スマホアプリ化も検討しているため、あえて分離しています。 現在はWebブラウザだけですが、PWAにも対応しているのでホーム画面にアプリを追加していただければアプリのような操作感でお使いいただけます。 今回初めてAWSを使用しましたが、ほかのクラウドサービスと比べて非常に使いやすい印象でした。 さすがクラウドサービス市場のトップです。ちなみに2位はAzureです。 G○Pだとなぜそうなる?みたいことになりがちだったので、これからはAWSを推していこうと思います。 ReactのTips Reactは以前から使用した経験があったのと、Adobeが提供しているReact Spectrumを使ってみたかったこともあり採用しました。 React Spectrumについては本記事では詳しく説明しないので、別の機会にまとめようと思っています。 Reactでクッキーを操作する方法 biby diaryではログインした時にAPIにリクエストを送るためのトークンを発行しているのですが、そのトークンを持っておくためにクッキーを使用しています。 Reactにはreact-cookiesというクッキーの操作が簡単にできるパッケージがあります。 https://github.com/reactivestack/cookies/tree/master/packages/react-cookie 利用例 まずはアプリ全体をCookiesProviderラップします。 import React from 'react'; import App from './App'; import { CookiesProvider } from 'react-cookie'; export default function Root() { return ( <CookiesProvider> <App /> </CookiesProvider> ); } アプリを登録する場合は下記のようになります。 aaa import React from 'react'; import { useCookies } from 'react-cookie'; import NameForm from './NameForm'; function App() { const [cookies, setCookie] = useCookies(['name']); function onChange(newName) { setCookie('name', newName, { path: '/' }); } return ( <div> <NameForm name={cookies.name} onChange={onChange} /> {cookies.name && <h1>Hello {cookies.name}!</h1>} </div> ); } export default App; useCookiesを使用したい箇所でインポートし、const [cookies, setCookie] = useCookies(['name']);でクッキーを登録できるようにします。 実際の登録は、setCookie('name', newName, { path: '/' });となっており、pathオプションを指定することでクッキーの保存対象にしたいページを選択できます。/を指定した場合はアプリのページ全てに対してクッキーを保存することになります。 ログアウト時などにクッキーを削除したい場合は、removeCookieを使用します。 removeCookieにもpathオプションを指定することができ、removeCookie('name', { path: '/' })としておくことでページ全体に対して選択したクッキーを削除できます。 あえて{ path: '/' }を指定しておくことをおすすめします。 環境変数の設定 Reactの構築にはCreate React Appを用いており、Create React Appにはデフォルトで環境変数を供給する機構が備わっています。 使い方は簡単でプロジェクト直下に.envファイルを作成し、REACT_APP_をプレフィックスにした変数を入れておくことでReactアプリ内で呼び出すことができます。 呼び出す時はprocess.env.REACT_APP_[変数名]で変数を取得できます。 利用例 REACT_APP_TEST="test" 環境変数を呼び出す時はこのようになります。 const test = process.env.REACT_APP_TEST; amplifyでの環境変数設定 .envファイルは機密情報を含める場合もあるため.gitignoreに入れておくことが望ましく、デプロイ時に.envを生成する必要があります。 amplifyでは管理画面のサイドバーにある環境変数から設定を行い、同じくサイドバーのビルドの設定から.envファイルの生成コマンドを追記します。 環境変数設定画面を開くとこのような画面が出てきます。 変数と値をそれぞれ入力します。 続いてビルドの設定を開くと、下記のようなビルド設定が表示されていると思います。 amplify.yml version: 1 frontend: phases: preBuild: commands: - yarn install build: commands: - yarn run build artifacts: baseDirectory: build files: - '**/*' cache: paths: - node_modules/**/* .envの生成はbuild>commandsに- echo "REACT_APP_TEST=$REACT_APP_BACKEND_TEST" >> .envを追記します。 同じ画面からymlファイルの編集ができるので、そのまま追記できます。 画像アップロード 画像アップロードに手こずってしまったので、同じような境遇に会った方、会われるであろう方に向けてTipsを紹介しておきます。 基本的にAPIリクエスト時にデータを渡すときは、JSON形式にしているのですが画像をアップロードする時にどうしてもJSONではリクエストに失敗するということがありました。 調べるとJSON形式でもファイル送信ができるみたいな情報が出てきたのですが、実現できなかったのでファイルアップロードによく使われるmultipart/form-data形式を使うことにしました。 最終的にmultipart/form-dataで無事に画像ファイルは送信できるようになったのですが、もう一つ躓いたポイントがあり、リクエストヘッダのcontent-typeに明示的にmultipart/form-dataを指定するとリクエストに失敗する現象が発生しました。 理由はいまだにわからないので、もし分かる方いたら教えてください。 DjangoのTips DjangoにはDjango Rest FrameworkというREST APIを実装できるパッケージがあるのですが、これがかなり使いやすく容易にAPIを開発できるのでDjangoを採用しました。 デフォルトで管理画面が付いている点も理由の1つです。 CORS問題 API開発でやっかいなのが、CORS問題です。CORSは同一生成元ポリシーと呼ばれるブラウザのセキュリティ仕様で、あるサーバーにWebからリクエストを送る時に同じドメインであればエラーは起きないのですが、違うドメインからのリクエストは脆弱性から守るために拒否されます。 このサイトがわかりやすくCORS問題について説明しているので、気になる方はご参照ください。 https://coliss.com/articles/build-websites/operation/work/cs-visualized-cors.html CORS問題を解消するにはバックエンド側でCORSの制御をする必要があります。 corsheadersというパッケージを使うことで、CORS問題を片付けることができます。 https://github.com/adamchainz/django-cors-headers 環境変数 Djangoは環境を立ち上げた時にシークレットキーが作られます。このシークレットキーはユニークIDの生成などに使用されるので、gitで管理するようなことはしたくありません。 そのため、環境変数で持っておくことが望ましいです。 Djangoには、django-environという環境変数を設定するためのパッケージがあります。 https://github.com/joke2k/django-environ このパッケージを使うことで容易に環境変数の設定ができます。 使い方 プロジェクト直下に.envファイルを作成し変数を追記していきます。 具体的にはこのような形で追記します。 代入する値はダブルクオートなどで囲む必要はないのでご注意ください。 DEBUG=on SECRET_KEY=your-secret-key 呼び出す時はこのようになります。 settings.py import environ env = environ.Env( # set casting, default value DEBUG=(bool, False) ) # reading .env file environ.Env.read_env() # False if not in os.environ DEBUG = env('DEBUG') # Raises django's ImproperlyConfigured exception if SECRET_KEY not in os.environ SECRET_KEY = env('SECRET_KEY') ngrok アプリを開発しているとスマホでも実際に挙動を確認したいということがあると思うのですが、フロントエンドとバックエンドを分離していると、ローカル環境ではAPIのドメインがlocalhostなため確認できません。 フロントにはアクセスできるけどバックエンドにはつながらないといったことになると思います。 このときにおすすめなのがngrokという開発ツールで、ngrokを使用することで一時的に作られるドメインとローカルのドメインを結びつけることができ、他のデバイスからアクセスすることができます。 https://ngrok.com/ 開発途中のアプリをngrokを使ってクライアントに見てもらうこともできるので、実務でも大活躍します。 会員登録なしでも使用することができるのですが、その場合一時的に発行されるドメインの有効期限が決まっています。 有効期限なしで使用したい場合は、無料プランの登録もできるので会員登録しておくことをおすすめします。 起動はターミナルを開いてngrok http ポート番号と入力するだけなので簡単にアプリを外部に向けて公開できます。 今後の計画 biby diaryはまだまだ未完成なので、継続的に開発をしていきます。 今は無料プランしかないので近々有料プランを追加する予定しているほか、bibyユーザーがペットへの寄付を外部に募ることができる機能も考えています。 追加して欲しい機能も絶賛募集中なので、ぜひご連絡ください。本記事のコメント欄もしくは、こちらのお問い合わせフォームからご要望いただくこともできます。 https://forms.gle/EJdXYLNWbeLfCccm9 形だけですがロードマップを公開しているので、気になる方はぜひご覧ください。 https://uichi.notion.site/5bead4691e344d6b946a09f9aa8c7d1f?v=3fccb1b559ef4c27ab0872db33a95690![FireShot Capture 011 - biby diary - biby-diary.studio.site.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243199/a2f02cc1-e8cb-d6ff-3391-6fb338d10024.png)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptで扱う連想配列のあれこれ(GET / CREATE系)

はじめに 連想配列であんなことやこんなことをしたい人向けのきじになります。 バックエンドで整形して返すのが一番ですが、javascriptで処理したいという方は参考までに。 (GET系)連想配列から特定の値を含む配列を取得 index.js var array = [ {id:1, name:"hoge",price:3000, status:1}, {id:2, name:"test",price:300, status:1}, {id:3, name:"hello",price:1500, status:1}, {id:4, name:"world",price:3000, status:2}, {id:5, name:"hoge",price:200, status:1}, {id:6, name:"test",price:400, status:1}, {id:7, name:"hoge",price:4000, status:2} ]; // 1つの検索条件で取得 var select_id = 3; const callback = item => item.id == select_id; // 検索条件 var result = array.filter(item => callback(item)); console.log(result); // {id:3, name:"hello",price:1500, status:1} // 複数の検索条件で取得 var select_name = "hoge"; var select_price = 1000; const callback = item => item.name == select_name && item.price >= select_price; var result = array.filter(item => callback(item)); console.log(result); // {id: 1, name: "hoge", price: 3000, status: 1} {id: 7, name: "hoge", price: 4000, status: 2} // 配列内のインデックスを取得(単体) var select_id = 4; const index = array.findIndex(item => item.id === 4); console.log(index); // 3 // 配列内のインデックスを取得(複数) var select_name = "hoge"; var result = []; var exam_array = array; for (var i = 0; i < exam_array.length; i++){ var index = exam_array.findIndex(item => item.name == select_name); // 複数条件は&&でつなげる if(index != -1){ console.log(index); result.push(index); exam_array.splice(index,1,""); } else { console.log("complete"); break; } } console.log(result); // [0, 4, 6] (CREATE系)配列に新しい値を追加 index.js var array = [ {id:1, name:"hoge",price:3000, status:1}, {id:2, name:"test",price:300, status:1}, {id:3, name:"hello",price:1500, status:1}, {id:4, name:"world",price:3000, status:2}, {id:5, name:"hoge",price:200, status:1}, {id:6, name:"test",price:400, status:1}, {id:7, name:"hoge",price:4000, status:2} ]; // 配列の最後に追加 var new_data = {'id':7, 'name':"new_data",'price':5000, 'status':1}; array.push(new_data); console.log(array); // (8) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] // 配列の最後に追加(auto_inclement版) var last_id = array[array.length -1].id; var new_data = {'id':last_id + 1, 'name':"new_data",'price':5000, 'status':1}; array.push(new_data); console.log(array); // (8) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] <- id:8のitem // 任意の場所に値を追加(ユニークじゃなくてもいい場合) var put_position = array[5 - 2]; // 前から5番目に追加したい場合,先に4番目の配列(直前の配列)を取得する var put_id = put_position.id; var new_data = {'id':put_id + 1, 'name':"new_data",'price':5000, 'status':1}; array.splice(put_id,0,new_data); console.log(array); // ...{id: 4, name: "world", price: 3000, status: 2} // {id: 5, name: "new_data", price: 5000, status: 1} // {id: 5, name: "hoge", price: 200, status: 1} // {id: 6, name: "test", price: 400, status: 1}... // 任意の場所に値を追加(後の値が自動調整版) var put_position = array[5 - 2]; // 前から5番目に追加したい場合,先に2番目の配列(直前の配列)を取得する var put_id = put_position.id; // 直前の配列のidを取得する var new_data = {'id':put_id, 'name':"new_data",'price':5000, 'status':1}; array.splice(put_id,0,new_data); var exam_array = array; const new_index = exam_array.findIndex(item => item.id === new_data.id); var before_array = exam_array.slice(0,new_index + 1); var adjust_array = exam_array.slice(new_index + 1, exam_array.length); for(var j = 0; j < adjust_array.length; j++){ adjust_array[j].id = adjust_array[j].id + 1; } var update_array = before_array.concat(adjust_array); array = update_array; console.log(array); // ...{id: 4, name: "world", price: 3000, status: 2} // {id: 5, name: "new_data", price: 5000, status: 1} // {id: 6, name: "hoge", price: 200, status: 1} // {id: 7, name: "test", price: 400, status: 1} // {id: 8, name: "hoge", price: 4000, status: 2}... 次回はDELETEとUPDATEも紹介します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Amplify の Todo アプリチュートリアルをやってみた

目的 こちらのAWS Amplify チュートリアルを通して AWS の Amplify に触れ、主にどういったサービスがあるのかをざっくりと知る事。 主な使用技術 React Redux Toolkit Material-Ui React-Hook-Form Amplify 公式サイトでは言語がJS、状態管理がHooks、入力フォームがuseStateでの管理となっておりました。 学習目的の為少しでもオリジナリティーを持たせることを考え、言語をTS、状態管理をRedux Toolkit、入力フォームをReact-Hook-Formに変更し簡単なバリデーションを付けました。 UI ログイン / サインアップ 使用したAmplifyのサービス API Auth Hosting 認証のためのログイン・サインアップページについて 自分でデザインなども含めてログインページをどうしても作らなければいけない場合でなければ、amplifyのサービスでコンポーネントが用意されているのでインポートすればそのまま使えてしまいます。 使用したいコンポーネントをインポートして AmplifyAuthenticatorでラップします。 今回はサインアップに必要なユーザーデータを、Username, Email, Passwordの3つにしました。 App.tsx import Amplify from 'aws-amplify'; import { AmplifyAuthenticator, AmplifySignUp, AmplifySignOut, } from '@aws-amplify/ui-react'; const App: React.VFC = () => { return return ( <AmplifyAuthenticator> <AmplifySignUp slot="sign-up" formFields={[ { type: 'username', }, { type: 'email', inputProps: { required: true, autocomplete: 'username' }, }, { type: 'password', inputProps: { required: true, autocomplete: 'new-password' }, }, ]} /> <AmplifySignOut /> </AmplifyAuthenticator> ); } GraphQL これまでAPI通信を行う際には、基本的なHTTPメソッドである get, post, put, delete などを用いてコードを書いてきましたが、GraphQLでは記述の仕方が異なります。 Query getに当たる部分 Mutation post, put, deleteに当たる部分。 引数などを用いて変更を加える。 Subscription 情報を観察し、自動的に変更を反映させる。Twitterのタイムラインなどに使われる機能。 (今回は使用しない) これら全て初めて見るものでありそれらがどういったものなのかを知る為にドキュメントを最初にひたすら読み込む必要があった。しかし今回のようなTodoアプリ程度であれば、Subscription機能を用いないのでQueryとMutationの使い方を公式ドキュメントで参考しデータの取得、更新を行う事ができた。 感想 GraphQLでは型関係の対処の所がとても辛い部分であり、どうしても解決できない部分はanyを使用してしまっている部分があるので、今後アップデートしていきたい。 ただ最終的になんとか動くものは作れた事と、amplifyのサービスを実際に使用して大まかにでも知ることができた事は今後にとても役に立つと感じた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactNativeでカスタムフォントを使用する

備忘録 関数コンポーネントでカスタムフォントを使っている記事が少なかったのでメモ スマホで見るとこんな感じ 環境 使用プラットフォーム:ReactNative + Expo 使用フォント:Yomogi(assetsフォルダ内に入れています) カスタムフォントの導入 App.js import { StatusBar } from 'expo-status-bar'; import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { NavigationContainer } from '@react-navigation/native'; import { HomeScreen } from './src/Home'; export default function App() { const Drawer = createDrawerNavigator(); return ( <NavigationContainer> <StatusBar hidden /> <Drawer.Navigator initialRouteName="Home"> <Drawer.Screen name="Home" component={HomeScreen} /> </Drawer.Navigator> </NavigationContainer> ); } DrawerNavigatorを使って初期状態にHomeScreenを表示しています。 HomeScreenの特定箇所のテキストにカスタムフォントを適用したいので、Home.jsで作業していきます。 home.js import React, { useEffect, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import * as Font from 'expo-font'; export const HomeScreen = () => { const navigation = useNavigation(); const [fontLoaded, setFontLoaded] = useState(false); useEffect(() => { const subscription = navigation.addListener('focus', async () => { await Font.loadAsync({ 'Yomogi': require('../assets/Yomogi-Regular.ttf') }).then(() => { setFontLoaded(true); }) }); return subscription; }, []); return ( <View style={styles.container}> {fontLoaded && <Text style={styles.hello}>こんにちは</Text> } </View> ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'pink', alignItems: 'center', justifyContent: 'center', }, hello: { fontSize: 70, fontFamily: 'Yomogi', } }); カスタムフォントをロードするためにはexpo-fontのloadAsyncを使います。今回の場合は、HomeScreenにfocusした際にロードを行うようにしました。また、フォントがしっかりロードされてからテキストが描画される必要があるため、fontLoadedがtrueの時にテキストが表示されるようにしています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactメモ

propsの分割代入 propsは最初の段階で分割代入しておくと、それ以降で毎回props.みたいな記述を行う必要がないのでいい感じ const App = (props) => { const { name, age } = props; return ( <div> <div>{name}</div> <div>{age}</div> <div> ) } みたいに書くことができる。 オブジェクトのプロパティの省略記法 オブジェクトのキーとプロパティが同名の場合は省略して記述することができる。 const TestObj = { color, //みたいに書くことができる。 fontSize: "18px" }; useStateの使い方 reactライブラリからuseStateを取り出してそこからstateを分割代入する形で値を使用する import React from "react"; import { useState } from "react"; const App = () => { //カウントアップのstate const [num, setNum] = useState(1); //true、falseを切り替えるstate const [face, setFace] = useState(true); const onClickAdd = () => { //クリックされた時に+1をする関数 setNum(num + 1); }; const onClickSwitchFace = () => { //true, falseを切り替える関数 setFace(!face); }; return( <div> {/* state表示 */} <div>{num}</div> {/* クリックされた時にカウントアップする関数を呼び出し */} <button onClick={onClickAdd}>ボタン</button> {/* faceがfalseの時に顔文字を表示 */} {face && <div>( ; ; )</div>} {/* クリックされた時にtrue,falseを切り替える関数を呼び出し */} <button onClick={onClickSwitchFace}>on/off</button> </div> ) } export default App useEffectの使い方 関数の関心を切り分けるために使用する マウンティング時にのみ実行(コンポーネントが最初にレンダリングされた時のみ) useEffect(() => { console.lot("useEffect"); }, []) マウンティング時とnumのstateが変更された時に実行 const [num, setNum] = useState(); useEffect(() => { console.lot("useEffect"); }, [num]) if文の省略形 例えば、空文字がstateにsetStateされないようにしたい場合は以下のように記述することができる。 const onPushTodo = () => { //if文の省略形 if (todoText === "") return; const pushTodo = [...inCompleteToDos, todoText]; setInCompleteToDos(pushTodo); setTodoText(""); };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【FC版】React + Rails API + axios + react-router-domでCRUDを実装する

こんにちは!スージーです。 以前書いたこちらの記事はClass Componentで書いた記事だったので、関数コンポーネント(Functional Component)で書き直してみました。関数コンポーネントで書くにあたり、hookを使って実装します やりたい事 CRUD(新規作成・一覧表示・詳細表示・更新・削除)を実装します やらない事 ログイン周りの実装 Rails側 アソシエーションを使ったモデルのリレーション モデルバリデーション CORSの説明 React側 クライアントバリデーション hooksの説明 css その他 コマンドの説明 SQLの説明 ログの説明 こんな書き方すればReact × Rails APIでCRUDが実装できるんだなーって感じで見ていただけると幸いです 開発環境 Ruby 2.7.1 Rails 6.0.4 MySQL node.js 14.8.0 React 17.0.2 参考 React:関数コンポーネントとクラスコンポーネントの違い React hooksを基礎から理解する (useEffect編) React hooksを基礎から理解する (useState編) まずAPI(Rails)側から実装開始 以前の記事とほぼ同じなので色々、割愛します mkdir react-form-sample && cd react-form-sample rails new backend -d mysql --api && cd backend gem 'rack cors'をインストールとモデル・コントローラを作成 gem 'rack-cors' bundle install rails g model post name:string neko_type:string rails db:create rails db:migrate rails g controller api/v1/posts routes.rb Rails.application.routes.draw do namespace :api do namespace :v1 do resources :posts end end end cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:3001' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end Front(React)側は3001ポートで繋ぐのでoriginsは3001を許可します posts_controller.rb class Api::V1::PostsController < ApplicationController def index render json: Post.all end def show render json: Post.find(params[:id]) end def create post = Post.new(post_params) if post.save render json: post else render json: post.erros, status: 422 end end def update post = Post.find(params[:id]) if post.update(post_params) render json: post else render json: post.errors, status: 422 end end def destroy post = Post.find(params[:id]) post.destroy render json: post end private def post_params params.require(:post).permit(:name, :neko_type) end end seed.rb Post.create!(name: 'ニャア', neko_type: 'アメリカンショートヘア') Post.create!(name: 'まる', neko_type: 'スコッティシュフォールド') Post.create!(name: 'むぎ', neko_type: 'スコッティシュフォールド') curlコマンドで確かめる為にサンプルデータを作ります api動作確認 各エンドポイントは以下のようになっています rails routes api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index POST /api/v1/posts(.:format) api/v1/posts#create api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show PATCH /api/v1/posts/:id(.:format) api/v1/posts#update PUT /api/v1/posts/:id(.:format) api/v1/posts#update DELETE /api/v1/posts/:id(.:format) api/v1/posts#destroy rails db:seed rails s // もう一つタブを開く // index curl http://localhost:3000/api/v1/posts [{"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-24T15:01:05.371Z","updated_at":"2021-07-24T15:01:05.371Z"},{"id":2,"name":"まる","neko_type":"スコッティシュフォールド","created_at":"2021-07-24T15:01:05.376Z","updated_at":"2021-07-24T15:01:05.376Z"},{"id":3,"name":"むぎ","neko_type":"スコッティシュフォールド","created_at":"2021-07-24T15:01:05.381Z","updated_at":"2021-07-24T15:01:05.381Z"}] // show curl http://localhost:3000/api/v1/posts/1 {"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-24T15:01:05.371Z","updated_at":"2021-07-24T15:01:05.371Z"} // create curl -X POST http://localhost:3000/api/v1/posts -d "[name]=test&[neko_type]=test" {"id":4,"name":"test","neko_type":"test","created_at":"2021-07-24T15:36:01.040Z","updated_at":"2021-07-24T15:36:01.040Z"} // update curl -X PATCH http://localhost:3000/api/v1/posts/4 -d "[name]=update&[neko_type]=update" {"id":4,"name":"update","neko_type":"update","created_at":"2021-07-24T15:36:01.040Z","updated_at":"2021-07-24T15:36:31.170Z"} // delete curl -X DELETE http://localhost:3000/api/v1/posts/4 {"id":4,"name":"update","neko_type":"update","created_at":"2021-07-24T15:36:01.040Z","updated_at":"2021-07-24T15:36:31.170Z"} jsonが正常に返ってきました。アクションは正常に動いている事が確認できました。これでapi側は完了です 次にClient(React)側の実装開始 react-form-sample $ create-react-app frontend cd frontend npm install axios react-router-dom axios-case-converter 今回使うライブラリは以下の通りです axios HTTPクライアント用のライブラリ react-router-dom ルーティング設定用のライブラリ axios-case-converter axiosで送受信する値をスネークケース<=>キャメルケースに変換するライブラリ 使わないファイルを削除とjsファイルをjsxファイルに変換 rm src/App.css src/App.test.js src/logo.svg src/reportWebVitals.js src/setupTests.js mv src/App.js src/App.jsx mv src/index.js src/index.jsx index.jsxとApp.jsxを修正 // index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // App.jsx import React from 'react'; const App = () => { return <h1>Hello World</h1>; }; export default App; npm startでlocalhost:3000にアクセスして「Hello World」が表示されればOK API Clientを作成 各コンポーネントでaxiosを使ってapiコールしても良いのですが、今回はclient.jsファイルとpost.jsファイルを作成してapiコール部分をまとめます。 mkdir -p src/lib/api touch src/lib/api/client.js touch src/lib/api/post.js // client.js import applyCaseMiddleware from 'axios-case-converter'; import axios from 'axios'; // ヘッダーに関してはケバブケースのままで良いので適用を無視するオプションを追加 const options = { ignoreHeaders: true, }; const client = applyCaseMiddleware( axios.create({ baseURL: 'http://localhost:3000/api/v1', }), options ); export default client; // post.js import client from './client'; // 一覧 export const getList = () => { return client.get('/posts'); }; // 詳細 export const getDetail = (id) => { return client.get(`/posts/${id}`); }; // 新規作成 export const createPost = (params) => { return client.post('/posts', params); }; // 更新 export const updatePost = (id, params) => { return client.patch(`/posts/${id}`, params); }; // 削除 export const deletePost = (id) => { return client.delete(`/posts/${id}`); }; 一覧画面を作成 /は一覧画面とします // App.jsx import React from 'react'; import { BrowserRouter as Router, Switch, Route, } from 'react-router-dom'; import List from './components/List'; const App = () => { return ( <Router> <Switch> <Route exact path='/' component={List} /> </Switch> </Router> ); }; export default App; // List.jsx import React from 'react'; const List = () => { return <h1>HOME</h1>; }; export default List; localhost:3000にアクセスするとHOMEとなっていればOK api側との疎通確認をする 先程作成したlib/api/post.jsが正常に動くか疎通確認をします // List.jsx import React, { useEffect, useState } from 'react'; import { getList } from '../lib/api/post'; const List = () => { const [dataList, setDataList] = useState({}); useEffect(() => { handleGetList(); }, []); const handleGetList = async () => { try { const res = await getList(); console.log(res.data); setDataList(res.data); } catch (e) { console.log(e); } }; return <h1>HOME</h1>; }; export default List; api側をrails s起動、client側をnpm startして立ち上げます backend $ rails s // 別タブで frontend $ npm start Would you like to run the app on another port instead? › (Y/n) Y // Y + enterで3001ポートでアクセスします console.logでapiコールで取得したデータがdevツールに表示されればOK apiから取得した一覧を表示する // List.jsx import React, { useEffect, useState } from 'react'; import { getList } from '../lib/api/post'; import { useHistory, Link } from 'react-router-dom'; const List = () => { const [dataList, setDataList] = useState([]); useEffect(() => { handleGetList(); }, []); const handleGetList = async () => { try { const res = await getList(); console.log(res.data); setDataList(res.data); } catch (e) { console.log(e); } }; return ( <> <h1>HOME</h1> <button>新規作成</button> <table> <thead> <tr> <th>名前</th> <th>猫種</th> <th colSpan='1'></th> <th colSpan='1'></th> <th colSpan='1'></th> </tr> </thead> {dataList.map((item, index) => ( <tbody key={index}> <tr> <td>{item.name}</td> <td>{item.nekoType}</td> <td> <Link to={`/edit/${item.id}`}>更新</Link> </td> <td> <Link to={`/post/${item.id}`}>詳細へ</Link> </td> <td> <button>削除</button> </td> </tr> </tbody> ))} </table> </> ); }; export default List; 更新ボタン・詳細リンク・削除ボタン・新規作成ボタンはこの後、実装します localhost:3001で見ると以下のようにデータが表示されていればOK 詳細画面を作成 次に詳細画面を実装していきます。 詳細画面は一覧画面から1レコードを取得 そのレコードのidをreact-router-domを使ってqueryパラメータから取得 取得したidをapiクライアントからサーバへリクエスト レスポンスを受け取る という流れを実装します まずDetailコンポーネントを作成します mkdir src/components/Detail.jsx 詳細画面のパスはlocalhost:3001/post/1とします // App.jsx import React from 'react'; import { BrowserRouter as Router, Switch, Route, } from 'react-router-dom'; import List from './components/List'; // 追加 import Detail from './components/Detail'; const App = () => { return ( <Router> <Switch> <Route exact path='/' component={List} /> <Route path='/post/:id' component={Detail} /> // 追加 </Switch> </Router> ); }; export default App; // Detail.jsx import React, { useEffect, useState } from 'react'; import { getDetail } from '../lib/api/post'; import { useHistory, useParams } from 'react-router-dom'; const Detail = (props) => { const [data, setData] = useState({}); // { id: "1" }を取得する const query = useParams(); // 戻るボタン用 const history = useHistory(); // 画面描画時にidがundefinedだとデータ取得できないので // 依存配列にidを入れて、idがundifined => 1と更新された時に     // useEffectの副作用を使い、処理をもう一度実行させる useEffect(() => { handleGetDetail(query); }, [query]); const handleGetDetail = async (query) => { try { const res = await getDetail(query.id); console.log(res.data); setData(res.data); } catch (e) { console.log(e); } }; return ( <> <h1>DETAIL</h1> <div>ID:{data.id}</div> <div>名前:{data.name}</div> <div>猫種:{data.nekoType}</div> <button onClick={() => history.push('/')}>戻る</button> </> ); }; export default Detail; localhost:3001/1で見ると以下のようにデータが表示されていればOK 削除機能を実装 一覧ページに配置した削除ボタンで掴んだレコードのデータを物理削除します 一覧画面から1レコードを取得 onClickイベントでidを取得 取得したidをapiクライアントに渡しdeleteリクエストをサーバに送る 削除したレコード以外を再描画する // List.jsx import React, { useEffect, useState } from 'react'; // deletePostを追加 import { getList, deletePost } from '../lib/api/post'; import { Link } from 'react-router-dom'; const List = () => { const [dataList, setDataList] = useState([]); useEffect(() => { handleGetList(); }, []); const handleGetList = async () => { try { const res = await getList(); console.log(res.data); setDataList(res.data); } catch (e) { console.log(e); } }; // 削除する関数を追加 const handleDelete = async (item) => { // 引数にitemを渡してitem.idで「1」など取得できればOK console.log('click', item.id) try { const res = await deletePost(item.id) console.log(res.data)             // データを再取得 handleGetList() } catch (e) { console.log(e) } } return ( <> <h1>HOME</h1> <button>新規作成</button> <table> <thead> <tr> <th>名前</th> <th>猫種</th> <th colSpan='1'></th> <th colSpan='1'></th> <th colSpan='1'></th> </tr> </thead> {dataList.map((item, index) => ( <tbody key={index}> <tr> <td>{item.name}</td> <td>{item.nekoType}</td> <td> <Link to={`/edit/${item.id}`}>更新</Link> </td> <td> <Link to={`/${item.id}`}>詳細へ</Link> </td> <td> {/* 追加 */} <button onClick={() => handleDelete(item)}>削除</button> </td> </tr> </tbody> ))} </table> </> ); }; export default List; localhost:3001で削除ボタンを押下後、レコードが1件削除できればOK railsのログを見るとdeleteメソッドでidを受け取り、deleteが実行されているのが分かります Started DELETE "/api/v1/posts/4" for ::1 at 2021-07-27 10:02:20 +0900 Processing by Api::V1::PostsController#destroy as HTML Parameters: {"id"=>"4"} Post Load (3.4ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 4 LIMIT 1 ↳ app/controllers/api/v1/posts_controller.rb:30:in `destroy' (23.6ms) BEGIN ↳ app/controllers/api/v1/posts_controller.rb:31:in `destroy' Post Destroy (0.4ms) DELETE FROM `posts` WHERE `posts`.`id` = 4 ↳ app/controllers/api/v1/posts_controller.rb:31:in `destroy' (2.1ms) COMMIT ↳ app/controllers/api/v1/posts_controller.rb:31:in `destroy' Completed 200 OK in 50ms (Views: 3.3ms | ActiveRecord: 29.4ms | Allocations: 2700) 新規作成画面を作成 次に新規作成を作成します。 touch src/components/New.jsx src/components/Form.jsx フォーム部分は編集画面でも使うのでForm.jsxを作成して、新規登録画面と共通化します テキストフィールドの入力された値をonChangeで検知 eventでname属性とvalue属性を取得 {name: hoge, nekoType: fuga}の連想配列の形でstateに保持 stateをapiクライアントに渡しpostリクエストをサーバに送る 新規作成画面のパスはlocalhost:3001/newとします ルータに追加 // App.jsx import React from 'react'; import { BrowserRouter as Router, Switch, Route, Redirect, } from 'react-router-dom'; import List from './components/List'; import Detail from './components/Detail'; // 追加 import New from './components/New'; const App = () => { return ( <Router> <Switch> <Route exact path='/' component={List} /> <Route path='/post/:id' component={Detail} /> <Route exact path='/new' component={New} /> // 追加 </Switch> </Router> ); }; export default App; 一覧ページの新規登録ボタンにイベント追加 // List.jsx import React, { useEffect, useState } from 'react'; import { getList, deletePost } from '../lib/api/post'; // useHistoryを追加 import { useHistory, Link } from 'react-router-dom'; const List = () => { // 省略 return ( <> <h1>HOME</h1> {/* 追加 */} <button onClick={() => history.push('/new')}>新規作成</button> <table> <thead> <tr> <th>名前</th> <th>猫種</th> <th colSpan='1'></th> <th colSpan='1'></th> <th colSpan='1'></th> </tr> </thead> {dataList.map((item, index) => ( <tbody key={index}> <tr> <td>{item.name}</td> <td>{item.nekoType}</td> <td> <Link to={`/edit/${item.id}`}>更新</Link> </td> <td> <Link to={`/post/${item.id}`}>詳細へ</Link> </td> <td> <button onClick={() => handleDelete(item)}>削除</button> </td> </tr> </tbody> ))} </table> </> ); }; export default List; 新規登録コンポーネント作成 // New.jsx import React, { useState } from 'react'; import FormBody from './Form'; import { createPost } from '../lib/api/post'; import { useHistory } from 'react-router-dom'; const New = () => { const [value, setValue] = useState({}) const history = useHistory(); const handleChange = (e) => { setValue({ ...value, [e.target.name]: e.target.value }) } const handleSubmit = async (e) => { e.preventDefault(); try { const res = await createPost(value) console.log(res) history.push('/') } catch (e) { console.log(e) } } return ( <> <h1>NEW</h1> <FormBody handleChange={handleChange} handleSubmit={handleSubmit} value={value} buttonType='登録' /> </> ) }; export default New; フォーム部分のコンポーネント作成 このコンポーネントをNew.jsxにimportします // Form.jsx import React from 'react'; const Form = (props) => { const { handleChange, handleSubmit, value, buttonType } = props return ( <> <form> <div> <label htmlFor="name">猫の名前:</label> <input type="text" name="name" id="name" onChange={(e) => handleChange(e)} value={value.name}/> </div> <div> <label htmlFor="nekoType">猫種</label> <input type="text" name="nekoType" id="nekoType" onChange={(e) => handleChange(e)} value={value.nekoType}/> </div> <input type="submit" value={buttonType} onClick={(e) => handleSubmit(e)}/> </form> </> ) }; export default Form; localhost:3001/newにアクセスして、nameとnekoTypeを入力 => 登録ボタン押下 => 一覧画面へリダイレクト、新規登録したデータが表示されればOK rails側のログを見るとフロントからparameterを受け取って、postテーブルにinsertされている事が確認できます Started POST "/api/v1/posts" for ::1 at 2021-07-26 23:40:50 +0900 Processing by Api::V1::PostsController#create as HTML Parameters: {"name"=>"バニやん", "neko_type"=>"エキゾチックショートヘア", "post"=>{"name"=>"バニやん", "neko_type"=>"エキゾチックショートヘア"}} (16.9ms) BEGIN ↳ app/controllers/api/v1/posts_controller.rb:13:in `create' Post Create (4.1ms) INSERT INTO `posts` (`name`, `neko_type`, `created_at`, `updated_at`) VALUES ('バニやん', 'エキゾチックショートヘア', '2021-07-26 14:40:50.273036', '2021-07-26 14:40:50.273036') ↳ app/controllers/api/v1/posts_controller.rb:13:in `create' (0.9ms) COMMIT ↳ app/controllers/api/v1/posts_controller.rb:13:in `create' Completed 200 OK in 48ms (Views: 1.8ms | ActiveRecord: 21.9ms | Allocations: 2432) 今回は特にバリデーションやエラーハンドリングしていないので素のHTMLで書いてますが、react-hook-formなどフォーム用の多機能なライブラリも色々あります 更新画面を作成 更新処理でCRUDの実装は終了です 一覧ページに配置した更新リンクでレコードのidを持って遷移させます 更新画面は一覧画面から1レコードを取得 そのレコードのidをreact-router-domを使ってqueryパラメータから取得 取得したidをapiクライアントからサーバへリクエスト レスポンスを受け取る 受け取ったレスポンスをテキストフィールドにセット 取得したidとparameterをapiクライアントに渡しpatchリクエストをサーバに送る touch src/components/Edit.jsx ルータに追加 編集画面のパスはlocalhost:3001/edit/1とします import React from 'react'; import { BrowserRouter as Router, Switch, Route, Redirect, } from 'react-router-dom'; import List from './components/List'; import New from './components/New'; import Detail from './components/Detail'; // 追加 import Edit from './components/Edit'; const App = () => { return ( <Router> <Switch> <Route exact path='/' component={List} /> <Route path='/post/:id' component={Detail} /> <Route exact path='/new' component={New} /> {/* 追加 */} <Route path='/edit/:id' component={Edit}/> </Switch> </Router> ); }; export default App; 更新リンクは一覧画面作成時に記述済です // List.jsx <Link to={`/edit/${item.id}`}>更新</Link> 更新画面の作成 // Edit.jsx import React, { useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { updatePost, getDetail } from '../lib/api/post'; import FormBody from './Form'; const Edit = () => { // apiで取得したデータを管理する為のstate const [value, setValue] = useState({ name: '', nekoType: '', }) // 一覧からreact-router-domを使ってidを取得 const query = useParams(); const history = useHistory(); // 画面が描画された時、queryが更新された時に関数を実行 useEffect(() => { handleGetData(query) },[query]) // idをapiクライアントに渡し、/api/v1/posts/:idのエンドポイントからデータ取得 const handleGetData = async (query) => { try { const res = await getDetail(query.id) console.log(res.data) // 使う値のみstateにセットする setValue({ name: res.data.name, nekoType: res.data.nekoType, }) } catch (e) { console.log(e) } } // テキストフィールドの変更を検知し値を書き換えstateで管理 const handleChange = (e) => { setValue({ ...value, [e.target.name]: e.target.value }) } // 更新ボタン押下後、idとparameterをapiクライアントに渡しリクエストを投げる const handleSubmit = async (e) => { e.preventDefault() try { const res = await updatePost(query.id, value) console.log(res) // リクエストが成功したら'/'にリダイレクトさせる history.push('/') } catch(e) { console.log(e) } } return( <> <h1>Edit</h1> <FormBody handleChange={handleChange} handleSubmit={handleSubmit} value={value} buttonType='更新' /> </> ) } export default Edit フォーム部分は新規作成画面でも使ったForm.jsxを使います 一覧画面で選択したレコードの更新リンクから/edit/1に遷移してnameとnekoTypeのデータがテキストフィールドにセットされていればOK 修正して更新ボタンを押下、一覧画面にリダイレクト、データが更新されていればOK railsのログを見るとpatchリクエストでパラメータを受け取りupdate処理が実行されていることが分かります Started PATCH "/api/v1/posts/1" for ::1 at 2021-07-27 09:57:49 +0900 Processing by Api::V1::PostsController#update as HTML Parameters: {"name"=>"ニャアupdate", "neko_type"=>"アメリカンショートヘア", "id"=>"1", "post"=>{"name"=>"ニャアupdate", "neko_type"=>"アメリカンショートヘア"}} Post Load (0.9ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1 ↳ app/controllers/api/v1/posts_controller.rb:21:in `update' (0.3ms) BEGIN ↳ app/controllers/api/v1/posts_controller.rb:22:in `update' Post Update (29.4ms) UPDATE `posts` SET `posts`.`name` = 'ニャアupdate', `posts`.`updated_at` = '2021-07-27 00:57:49.761871' WHERE `posts`.`id` = 1 ↳ app/controllers/api/v1/posts_controller.rb:22:in `update' (3.6ms) COMMIT ↳ app/controllers/api/v1/posts_controller.rb:22:in `update' Completed 200 OK in 59ms (Views: 0.8ms | ActiveRecord: 34.1ms | Allocations: 3346) 以上でCRUDの実装が完了です!お疲れさまでした! 最後に Reactを業務で使い始めた時に、このCRUDに苦戦した事があったのと、リンクとか、画面間のidの受渡しとか、パラメータのapiクライアントへの渡し方など、自分で見返す為にもまとめてみました おわり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Firebase × Recoil を使った認証機能の実装方法

概要 Firebase AuthenticationとRecoilを使った、ユーザー認証部分の処理を紹介します。 状態管理ライブラリにRecoilを使うことで、redux tool kitやuseContextを使うよりもシンプルに実装することができます。 環境 windows10 VSCode npm アーキテクチャ コンポーネントは、3つあります。 App.tsx:起点となるコンポーネント Auth.tsx:認証を行うコンポーネント Main.tsx:ログインしたユーザーが閲覧できるコンポーネント 処理の流れ ① アプリケーションを起動したときにまずApp.tsxが呼ばれます。ログアウト状態から始まるので、Auth.tsxへ遷移します。 ② Firebase Authに登録しているユーザーを使って、ログインします。 ③ Firebase Authのログイン状態をオブザーバーで監視します。 ④ ③により、ログイン状態が変更されたときに、その状態をRecoilに保存します。 ⑤ Recoilの状態が変更されたことによって、コンポーネントが再レンダリングされます。 ⑥ App.tsxで再びログイン状態が評価されて、Main.tsxへ遷移します。 また、Main.tsxでログアウトした場合、オブザーバー処理によって④が実行されるので、Auth.tsxに遷移します。 Recoil Recoilとはなんぞや? Recoilの使い方 公式ドキュメント プロジェクトの作成 Firebase 以下を参考に、プロジェクトを作成してください。 Firebaseプロジェクトの作成 また、Authenticationの初期設定も行います。このとき、Mail & Passwordを選択します。(お好きな認証方法で大丈夫です) CRA(Create React Application) Reactアプリケーションは、CRA(Create React Application)を使って作成します。 プロジェクトフォルダを作成して、そこで以下を実行します。 cmd npx create-react-app . --template typescript プロジェクトに必要なパッケージをインストールします。 cmd npm i firebase recoil npm i -D @types/recoil コーディング Firebase インスタンスの作成 root直下に.env.localを追加します。ここに、Firebaseプロジェクトの環境変数を記入します。 CRAでは、環境変数名をREACT_APP_で始めることで、環境変数として自動的に認識されるようになります。 .env.local REACT_APP_FIREBASE_API_KEY="apiKey" REACT_APP_FIREBASE_AUTH_DOMAIN="authDomain" REACT_APP_FIREBASE_DATABASE_URL="https://<projectId>.firebaseio.com" REACT_APP_FIREBASE_PROJECT_ID="projectId" REACT_APP_FIREBASE_STORAGE_BUCKET="storageBucket" REACT_APP_FIREBASE_MESSAGING_SENDER_ID="messagingSenderId" REACT_APP_FIREBASE_APP_ID="appId" src配下にfirebase.tsを追加します。 src/firebase/firebase.ts import 'firebase/auth'; import firebase from 'firebase/app'; const firebaseConfig = { apiKey: process.env.REACT_APP_FIREBASE_API_KEY, authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL, projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.REACT_APP_FIREBASE_APP_ID }; if (!firebase.apps.length) firebase.initializeApp(firebaseConfig); export const auth = firebase.auth(); Recoil State ログイン状態を保存しておくための処理を追加します。 src/store/auth.ts import { atom } from 'recoil'; export const signInUserState = atom({ key: 'auth/signIn', default: { uid: '' } }); 保持する状態は、ログインユーザーのuidです。 Firebase Auth 処理 Firebase Authに関する処理を追加します。 src/firebase/authFunctions.ts import { useEffect } from 'react'; import { useRecoilState, useResetRecoilState } from 'recoil'; import { signInUserState } from '../store/auth'; import { auth } from './firebase'; /** * ユーザー認証する */ export const signIn = async (email: string, password: string) => { try { await auth.signInWithEmailAndPassword(email, password); } catch (error) { alert('サインイン認証に失敗しました。'); } }; /** * ユーザー登録する */ export const signUp = async (email: string, password: string) => { try { await auth.createUserWithEmailAndPassword(email, password); } catch (error) { alert('ユーザー登録に失敗しました。'); } }; /** * サインアウトする */ export const signOut = async () => { try { await auth.signOut(); } catch (error) { alert('サインアウトに失敗しました。'); } }; /** * SignInの状態を監視する */ export const useAuth = () => { const [signInUser, setSignInUser] = useRecoilState(signInUserState); const resetStatus = useResetRecoilState(signInUserState); useEffect(() => { const unSub = auth.onAuthStateChanged(authUser => { if (authUser) { setSignInUser({ uid: authUser.uid }); } else { resetStatus(); } }); return () => unSub(); }, [setSignInUser, resetStatus]); return signInUser; }; useAuthは、カスタムフックになっています。 onAuthStateChangedでユーザーのログイン状態を監視し、ログイン状態なら(authUserが取得できたら)、setSignInUserを使ってRecoil Stateにuidを保存します。ログアウト状態なら、resetStatusを使ってRecoil Stateの状態を初期化します。 公式ドキュメント コンポーネント Recoilを扱うためには、コンポーネントをRecoilRootで囲う必要あります。 src/index.tsx import './index.css'; import React from 'react'; import ReactDOM from 'react-dom'; import { RecoilRoot } from 'recoil'; import { App } from './components/App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> <RecoilRoot> <App /> </RecoilRoot> </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(); Recoilは、コンポーネント全体をRecoilRootで囲っても、Stateの変化に関係するコンポーネントしか再レンダリングされません。 src/components/App.tsx import React from 'react'; import { useAuth } from '../firebase/authFunctions'; import { Auth } from './Auth'; import { Main } from './Main'; export const App: React.FC = () => { const signInUser = useAuth(); return <>{signInUser.uid ? <Main /> : <Auth />}</>; }; App.tsxでカスタムフックを呼出し、uidの有無で、MainコンポーネントかAuthコンポーネントに遷移させます。 Main.tsxとAuth.tsxは、割愛します。 まとめ Recoilを使うと、Redux Tool Kitを使うより簡単に実装できました。 おまけ Authコンポーネントのテンプレート Material UIでAuth.tsxを作成する場合、テンプレートを使うと簡単に作成できます。 ログインユーザー(authUser)から取得できる値 本記事では、uidのみ取得していますが、他にも色々取得できます。 プロパティ 型 説明 uid 文字列 新しく作成されたユーザーに割り当てる uid。1~128 文字の文字列を指定します。指定されていない場合は、uid が自動的に生成される email 文字列 ユーザーのプライマリ メールアドレス emailVerified ブール値 ユーザーのプライマリ メールアドレスが確認されているかどうか。指定されていない場合、デフォルト値は false phoneNumber 文字列 ユーザーのメインの電話番号 password 文字列 ユーザーのハッシュ解除された未加工のパスワード。6 文字以上 displayName 文字列 ユーザーの表示名 photoURL 文字列 ユーザーの写真 URL disabled ブール値 ユーザーが無効かどうか。無効の場合は true、有効の場合は false (デフォルト)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS React useStateで取得する関数に関数を渡す場合の注意

知っておくべき動作があったのでメモとして残しておきます。 もともとは次の記事に記載されていた挙動を自分でも確認しようと思って いろいろ試したときに発見しました。 破壊的メソッドとミュータブル・イミュータブル [React,Javascript] - Qiita 動作確認コード create-react-app で作成したあとに次のように記載します。 App.js import logo from './logo.svg'; import './App.css'; import { useState } from "react"; function App() { const [num, setNum] = useState([0]); // // 1.正常動作 // const addNum = () => { // num.push(1); // setNum([...num]) // }; // // 2.正常動作 // const addNum = () => { // setNum((preNum) => { // const newNum = [...preNum]; // newNum.push(1); // return newNum; // }); // }; // 3.異常動作 const addNum = () => { setNum((preNum) => { preNum.push(1); console.log({preNum}) return [...preNum]; }); }; return ( <> <button onClick={addNum}>1増えるよ</button> <p>{num}</p> </> ); } export default App; useState取得関数にuseState取得値を指定する方法と、useState取得関数に関数を渡す方法とがあります。Reactのドキュメントのここに記載があります。 関数型の更新 フック API リファレンス – React 正常動作、とコメントに記載したコードは正常に動作します。動作結果は次のようになります。 0 0 1 0 1 1 0 1 1 1 異常動作、とコメントに記載したコードは次のようになります。ボタンを押すごとに2回pushが実行されてしまいます。 0 0 1 0 1 1 1 0 1 1 1 1 1 しかも内部に仕込んでいるconsole.logが1回しか呼び出されていないのに、2回pushされている動作になっていて「何これ?」と非常に驚きました。 React useState の所に解説がありました。 QiitaのQAで聞いて教えてもらいました。 [Q&A] ReactのuseStateを使ったstate更新関数に関数を渡したときの変な挙動について知りたい - Qiita Reactのドキュメントのこちらに説明があります。 strict モード – React strict モードでは自動的には副作用を見つけてはくれませんが、それらの副作用をほんの少し決定的にすることによって特定できる助けになります。これは、以下の関数を意図的に 2 回呼び出すことによって行われます。 補足 React 17 以降で、React は console.log() のようなコンソールメソッドを自動的に変更し、ライフサイクル関数の 2 回目のコールでログが表示されないようにします。これにより特定のケースで意図しない動作を引き起こすことがありますが、回避策も存在します。 なるほどそういう仕組なんですね。 回避策は const console_log = console.log; として、内部で console.log ではなく console_log を使えという単純なもの。 これを踏まえて調べてみると、create-react-app で作成したプロジェクトの index.js は次のようになっていました。 index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> <App /> </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(); 確かに StrictMode というのが適用されていて、こんな挙動になったというわけ。 React.StrictMode を解除すれば[3.異常動作]の書き方をしても発生しなくなっていた。がそのままでいいこともなく[1.正常動作][2.正常動作]のどちらかの書き方にしておいたほうがよいです。 [3.異常動作] での書き方は、渡された関数の引数は更新前のstateの値なので、その値に対してpushして変更をかけると、よくない副作用を起こすということらしい。 気をつけてコード書いておきましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む