20211205のReactに関する記事は12件です。

React Hook Form v6 でMaterial UIのSwitchに初期値が入らない

はじめに 初投稿になります。 業務でReact Hook Form v6を使っていて、かなり詰まったので投稿します。 現象 タイトルの通り、React Hook Form v6のdefaultValuesに設定した値が Material UIのSwitchに設定されないという事象が発生しました。 Switchだけでなく、ラジオボタンやチェックボックスでも同じだと思います。 import { Button, Switch, FormControlLabel } from "@material-ui/core"; import { Controller, useForm } from "react-hook-form"; const ReactHookFormTest = () => { const { handleSubmit, control } = useForm({ defaultValues: { switch1: true, switch2: true } }); return ( <> <form onSubmit={handleSubmit((data) => console.log(data))}> <Controller as={Switch} type="checkbox" name="switch1" control={control} /> <br /> <FormControlLabel control={ <Controller name="switch2" control={control} as={Switch} /> } label="aaa" /> <Button type="submit" color="primary"> 送信 </Button> </form> </> ); }; export default function App() { return ( <div className="App"> <ReactHookFormTest /> </div> ); } defaultValuesに両方trueを設定しているのに、スイッチがオフのままです...。 原因 React Hook Formのバージョンによる原因だったようです。 公式のGithubのリリース情報を調べてみると、チェックボックス系の仕様が v6.0.3から変わったようで、Controllerの中のasプロパティは使えないようです。 v6.0.3以上を使っている場合は、renderプロパティを使う必要があります。 ちなみに上記コードはv6.0.2以下のバージョンであれば、正常に動作しました。 対策 公式Githubで拾ってきたコードに変更してみた。 import { Button, Switch, FormControlLabel } from "@material-ui/core"; import { Controller, useForm } from "react-hook-form"; const ReactHookFormTest = () => { const { handleSubmit, control } = useForm({ defaultValues: { switch1: true, switch2: true } }); return ( <> <form onSubmit={handleSubmit((data) => console.log(data))}> <Controller render={(props) => ( <Switch onChange={(e) => props.onChange(e.target.checked)} checked={props.value} /> )} type="checkbox" name="switch1" control={control} /> <br /> <FormControlLabel control={ <Controller name="switch2" control={control} render={(props) => ( <Switch onChange={(e) => props.onChange(e.target.checked)} checked={props.value} /> )} /> } label="aaa" /> <Button type="submit" color="primary"> 送信 </Button> </form> </> ); }; export default function App() { return ( <div className="App"> <ReactHookFormTest /> </div> ); } 見事チェックが入りました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js の API で ip アドレスで rate limit をかける方法

はじめに Next.js の API routes で、一定期間内にリクエストを送れる回数を IP アドレスで制限(rate limit)する方法について調べたので、メモとして残しておきます。 ↑こちらの公式例を少し改良したものです。 ライブラリ インストール yarn add lru-cache request-ip yarn add -D @types/lru-cache @types/request-ip コード project/src/lib/limitChecker.ts import LRU from "lru-cache"; import type { NextApiResponse } from "next"; type CheckLimitFunc = () => { check: (res: NextApiResponse, limit: number, ipAddress: string) => Promise<void>; }; export const LimitChecker: CheckLimitFunc = () => { const tokenCache = new LRU<string, number>({ max: 500, // Max 500 users per interval maxAge: 1000 * 60 * 5, // 5分, }); return { check: (res, limit, token): Promise<void> => new Promise((resolve, reject) => { const tokenCount = tokenCache.get(token) || 0; const currentUsage = tokenCount + 1; tokenCache.set(token, currentUsage); const isRateLimited = currentUsage > limit; res.setHeader("X-RateLimit-Limit", limit); res.setHeader("X-RateLimit-Remaining", isRateLimited ? 0 : limit - currentUsage); return isRateLimited ? reject("Too Many Requests") : resolve(); }), }; }; project/src/pages/api/hello.page.ts // pageExtensions の設定をしているので .page.ts になっています import requestIp from "request-ip"; import type { NextApiRequest, NextApiResponse } from "next"; import { LimitChecker } from "@/lib/limitChecker"; const limitChecker = LimitChecker(); type Data = { text: string; clientIp: string; }; export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>): Promise<void> { const clientIp = requestIp.getClientIp(req) || "IP_NOT_FOUND"; try { await limitChecker.check(res, 3, clientIp); } catch (error) { console.error(error); res.status(429).json({ text: `Rate Limited`, clientIp: clientIp, }); return; } res.status(200).json({ text: `テキスト`, clientIp: clientIp, }); } おわりに 簡単にではありますが、API routes に対するリクエスト回数の制限をかける方法でした。 bot 等からの大量のリクエストに対する対策の一つにはなると思います? 参考にさせていただいたサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React18のAutomaticBatchingを試してみた

最近 React18beta がリリースされたので紹介していきます。 React18 の新機能 React18 で追加される大きな新機能は下記の通りです。 Concurrent Rendering(並行レンダリング) SSR support for Suspense(サスペンスの SSR サポート) Automatic Batching(自動バッチ処理) Selective hydration(選択的ハイドレーション) Built-in Cache(組み込みキャッシュ機構) 今回は自動バッチ処理を React17 と比較してみたいと思います。 インストール create-react-app ${プロジェクト名} npm install react@beta react-dom@beta AutomaticBatching(自動バッチ処理)を検証 今までは関数内でのみバッチ処理を行っていました。 しかし、React18 からはどこから発生したか関係なくすべてのレンダリングがバッチ処理されるようになります。 creat-react-app のコードを流用して実装します。 React18 を導入するためには ReactDOM.render から ReactDOM.createRoot への移行が必須です。 ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); ↓ const container = document.getElementById("root"); const root = ReactDOM.createRoot(container); root.render( <React.StrictMode> <App /> </React.StrictMode> ); 検証用のコードです。 onClick で clickHandler 関数を実行します。 clickHandler 関数は setTimeout で非同期でステートを更新します。 const App = () => { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); const [count3, setCount3] = useState(0); const clickHandler = () => { setTimeout(() => { setCount1((count) => count + 1); setCount2((count) => count + 1); setCount3((count) => count + 1); }, 500); }; console.log("rendering!!!"); return ( <div className="app"> <header className="app-header"> <img src={logo} className="app-logo" alt="logo" /> <p>{count1}</p> <p>{count2}</p> <p>{count3}</p> <div className="push" onClick={clickHandler}> push </div> </header> </div> ); }; export default App; 以前の React であれば非同期処理内のステート更新は別々のレンダリングでした。 しかし、React18 ではバッチ処理が行われ、一回でレンダリングしているのがわかります。 React17 React18 バッチ処理をオプトアウトできる API flushSync でバッチ処理をオプトアウトできます。 import { flushSync } from "react-dom"; const App = () => { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); const [count3, setCount3] = useState(0); const clickHandler = () => { setTimeout(() => { // setCount1だけバッチ処理されない ReactDOM.flushSync(() => { setCount1((count) => count + 1); }); setCount2((count) => count + 1); setCount3((count) => count + 1); }, 500); }; console.log("rendering!!!"); return ( <div className="app"> <header className="app-header"> <img src={logo} className="app-logo" alt="logo" /> <p>{count1}</p> <p>{count2}</p> <p>{count3}</p> <div className="push" onClick={clickHandler}> push </div> </header> </div> ); }; export default App; React18 flushSync のコールバック関数内に切り出せばバッチ処理されません。 さいごに React 18 における並行レンダリングはオプトインになるため、コンポーネントの振る舞いにおいてデフォルトで大きな破壊的変更があるということはなくなります。 いつものメジャーリリースの時に要する労力と大差ないレベルの最小限の書き換えで、あるいは書き換えゼロで、React 18 にアップグレードすることができます。 「React 18に向けてのプラン」より引用 公式に書かれている通り StrictMode で実装していればほぼゼロコストでマイグレーションが可能です。破壊的変更(過去のコードに影響する変更)はオプトインになり、 Automatic Batching などのあまり影響のない変更はデフォルトで有効になるそうです。 React18 のアップデートするだけでもパフォーマンス向上が見込めるので、まだ一般向けリリースまで数か月ありますがアップデートに向けて準備しておくのもいいかもしれません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ツールチップの表示・非表示を切り替えるためにstopPropagationを使った例

この記事について ツールチップを表示・非表示にする実装で詰んでたので忘れないようにメモ 文字だけだと分かりづらいので挙動イメージは下の方でcodepenにまとめておいた ツールチップ表示・非表示の仕様 ツールチップ内の「×」を押すと消える 「?」を押すと表示される ツールチップの外をクリックすると消える ツールチップが開くのは「?」を押したアイテムのみ NG挙動 連続で「?」をクリックしても反応しない ツールチップ内をクリックするとツールチップが消えてしまう See the Pen Untitled by kena-nk (@kena-nk) on CodePen. コードはこんな感じ function Base() { const [question, setQuestion] = React.useState(0); const toggleDescription = (e, name) => (question === name ? setQuestion(null) : setQuestion(name)); const closeDescription = () => { if (question !== null) setQuestion(null); }; return ( <div className="container" onClick={() => closeDescription()}> <div className="list"> <div className="item"> <div>項目1</div> <div className="wrapper"> <div className="far fa-question-circle" onClick={(e) => toggleDescription(e, 1)} /> {question === 1 && ( <div className="description"> <div>項目1の説明が入るよ</div> <div className="far fa-times-circle" onClick={(e) => toggleDescription(e, 1)} /> </div> )} </div> </div> ~~省略~~ </div> </div> ); } OK挙動 連続で「?」をクリックしても反応しない → 解決 ツールチップ内をクリックするとツールチップが消えてしまう → 解決 See the Pen ツールチップOK挙動 by kena-nk (@kena-nk) on CodePen. コードはこんな感じ function Base() { const [question, setQuestion] = React.useState(0); const toggleDescription = (e, name) => { e.stopPropagation(); // e.stopPropagation();追加したよ return question === name ? setQuestion(null) : setQuestion(name); }; const closeDescription = () => { if (question !== null) setQuestion(null); }; return ( <div className="container" onClick={() => closeDescription()}> <div className="list"> <div className="item"> <div>項目1</div> <div className="wrapper"> <div className="far fa-question-circle" onClick={(e) => toggleDescription(e, 1)} /> {question === 1 && ( // onClick={(e) => e.stopPropagation()}追加したよ <div className="description" onClick={(e) => e.stopPropagation()}> <div>項目1の説明が入るよ</div> <div className="far fa-times-circle" onClick={(e) => toggleDescription(e, 1)} /> </div> )} </div> </div> ~~省略~~ </div> </div> ); } 今回の原因 一番外側のDOMに仕掛けてるcloseDescription()がクリックのたびに実行されていることが問題点だった。 本当は内側のDOMごとに仕掛けているtoggleDescription()が発火されてほしいが、親のイベント実行が邪魔してうまくいってないという感じだった。 流れに起こすとこんな感じ↓ 1. 項目1の「?」クリック 2. toggleDescription() が発火して question に 1 がセットされる 3. closeDescription() が発火するが question は null ではないので何も起こらない 4. 項目2の「?」クリック 5. toggleDescription() が発火して question に 2 がセットされる 6. closeDescription() が発火して question は値がセットされている為 question に null セットされる なので今回は親の実行を阻止するstopPropagation()を仕掛けることで回避した。 まとめ イベント発火阻止系はpreventDefault()しか使ったことがなく色々疎かったので使う機会が来てよかった。めちゃめちゃ有難い関数・・! これ以外にも親のイベントを阻止する方法などあればどなたか教えてください?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Serverless Frameworkでニュースサイトの新着記事をLINEで通知するアプリを作る

アプリケーション構築を学習する一環として、RSSフィードで配信された記事をLINE Messaging API経由で通知するアプリをServerless Frameworkで作りました。 友達追加すると、 新着記事を定期配信したり、手動で取得したりすることができるアプリです。 成果物はGithubにアップロードしていますが、せっかくなのでアプリの概要と、ハマったポイントをこちらにも投稿しておこうと思います。(以下、Github上のREADME.mdのほぼ抜粋です) 概要 RSSFeedで配信された記事を、LINE Messaging API経由で購読する 購読対象の記事は、設定用サイトで追加・削除・有効化・無効化が可能 定期購読のほかに、LINEでメッセージを送ることで記事を手動購読することも可能 システム構成は以下の通り。AWS上にソースをデプロイし、LINE Messaging API・LIFFと連携させる。 環境 種類 バージョン OS Windows10 version 2004 Node 14.17.5 Serverless Framework 2.66.1 開発環境 VSCode & Powershell ソース Githubにアップしています。 使ったサービス・言語・フレームワーク サービス・言語等 詳細 言語・環境 バックエンドのロジックはすべてNode.js v14.17.5で構築。 フレームワーク フレームワークはServerless Framework v2.66.2を用いて、AWS上サービスをデプロイした。 また、いくつかプラグインを導入した(※1)。 LINE Messaging API LINE上でのメッセージを受信し、適切な応答をするためにLINE Messaging APIを導入。無料枠で利用するので、月1,000件のメッセージ送信数制限には要注意・・・。応答はFlex Message(カルーセル型)もしくはテキスト形式で実施。 LIFF LINE Front-end Framework (LIFF)を使って、設定用のWebサイトで、LINEログインを利用しユーザID・名前・プロフィール画像を取得。 データベース AWS DynamoDBにユーザごとの情報(※2)を格納。 UI React + React-Bootstrap + Fontawesomeで設定画面のUIを構築。 ※1 利用したServerless Frameworkのプラグインは以下の通り。 server/serverless.yml抜粋 plugins: - serverless-dynamodb-local #DynamoDB接続部分の開発・テスト用 - serverless-vpc-plugin #Lambda用のVPC作成 - serverless-offline #ローカルでの開発・テスト - serverless-layers #Lambda Layersを利用するためのプラグイン client/serverless.yml抜粋 plugins: - serverless-s3-sync #ビルドしたフロントエンドのS3アップロード - serverless-cloudfront-invalidate #CloudFrontのキャッシュ削除自動化 ※2 DynamoDBレコードの構成は以下の通り。 userId:ユーザID lastSubscribe:RSSフィードの最終配信日時 subscribeFeeds:購読するRSSフィード情報(ここではPublicKeyとDevelopersIOの2つ) name:ユーザ名 DynamoDBのレコード { "userId": "U0000000000000000000000000000000", "lastSubscribe": "2021-11-25T01:00:19.091Z", "subscribeFeeds": [ { "feedUrl": "https://www.publickey1.jp/atom.xml", "addedAt": "2021-11-22T12:23:51.240Z", "siteUrl": "https://www.publickey1.jp/", "lastModifiedAt": "2021-11-21T23:23:51.240Z", "lastAction": "added", "feedId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "title": "Publickey", "enabled": true }, { "feedUrl": "https://dev.classmethod.jp/feed/", "addedAt": "2021-11-22T05:24:35.168Z", "siteUrl": "https://dev.classmethod.jp", "lastModifiedAt": "2021-11-21T23:24:35.168Z", "lastAction": "added", "feedId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "title": "DevelopersIO", "enabled": true } ], "name": "XXX" } ハマったところ Missing Authentication Tokenエラー 開発中、sls offline startでローカル開発環境では問題なくバックエンドAPIが動作するのに、AWSにデプロイするとMissing Authentication Tokenエラーが返されるようになった。 特に認証トークンを求めるような仕様にはしてないはず・・・ということで調べてみると、公式回答曰く認証トークンがない時だけでなく存在しないリソースやメソッドにリクエストを投げた時もこのレスポンスになるとのこと。(だったら、いかにも認証トークンエラーっぽい文言にしないでほしいですが・・・) ただ、メソッドやリソースが存在しないといってもローカル開発環境では問題なく動作するし・・・と色々調べたところ、実は当時serverless.ymlに設定していた下記が誤っていることが判明。 functions: webhook: handler: functions/webhook.handler events: - http: method: ANY path: /webhook/{proxy+} #<=この部分 この設定を書いたとき、https://xxxxxxx/webhook/xxx-xxx-1234{uuid}のように、パスパラメータ付きでリクエストすることも想定していたのですが、どうも{proxy+}は空白はNGのようでした。ただ、serverless-offlineを使った開発環境では空白でも動作してしまうので、混乱してしまいました・・・。 改めて、下記のようにしたら動作することを確認。 functions: webhook: handler: functions/webhook.handler events: - http: method: ANY path: /webhook/{proxy+} - http: #追加 method: ANY #追加 path: /webhook #追加 その後、アプリ構築を進める中でパスパラメータを使うことがないと分かったので、現在の形で決着。 functions: webhook: handler: functions/webhook.handler events: - http: method: ANY path: "/webhook" https化 LIFFが連携するURL(Endpoint URL)が、httpsサイトでないと設定できないことが途中で判明。 本番環境については、S3にホスティングしているURLを直接指定するのではなく、Cloudfrontを追加でデプロイすることで、CloudfrontのURLはhttps化しているためそちらを指定することで解決した。 開発環境については、yarn startではどうしてもhttp://localhost:3000になってしまうと思っていたが、こちらのサイトを参照し.env.development.localでHTTPS = "true"を定義することで簡単にhttps化できることを知り、それで解決できた。 自分でオレオレ証明書を発行して・・・とそれなりに面倒な手順を踏もうかと思っていたので、助かりました。 outbound80の手動穴あけ RSSフィード配信サイトに情報を取りに行く際、LambdaがインターネットアクセスするためにVPC・NAT Gateway等の定義をする必要があったが、それらを簡単に実施するためにserverless-vpc-pluginを使って開発していた。 その後、Lambda上でテストをする中でRSSフィードをうまく取得できるサイトと、取得できないサイトがあることがわかった。 原因を調べていくとserverless-vpc-pluginで構築した中でLambdaに割り当てられるセキュリティグループは、インターネットへのアウトバウンドアクセスが443(https)しか許可されておらず、"http://~"の配信サイトにアクセスできていないことが判明(特に、画像データだけはhttps→httpにリダイレクトして配信しているようなサイトもあり、なかなか検知しづらかったです・・・)。 serverless.yml上に自分でVPC等のリソースを定義しようかとも考えたが、結局deploy.ps1上でLambda実行用のセキュリティグループにOutbound 80ポートの穴あけを追加で定義することで解決しました。 #deploy.ps1抜粋 $TEMP_SG = aws cloudformation describe-stacks --output text --stack-name feed-notify-dev --query 'Stacks[].Outputs[?OutputKey==`AppSecurityGroupId`].[OutputValue]' aws ec2 authorize-security-group-egress --group-id $TEMP_SG --ip-permissions IpProtocol=tcp, FromPort=80, ToPort=80, IpRanges='[{CidrIp=0.0.0.0/0,Description="HTTP ACCESS for RSS Feed Subscribe"}]' Write-Output "Outbound HTTP Access added to Security Group" TypeError: Cannot read property 'pipe' of undefined 開発している途中で、sls deployするとタイトルの通りのエラーが表示されるようになった。 調査した結果、私の場合はserverless-layersとserverless-s3-syncの両方を同一ディレクトリにインストールしていたことが原因と判明。(もともとserver/serverless.ymlでクライアント側のソースも含めて一括デプロイしていました) serverless-layersはserverディレクトリ、serverless-s3-syncはclientディレクトリにインストールし、それぞれのディレクトリに分割してserverless.ymlを定義たうえでdeploy.ps1を用いて一括実行する形に見直すことで解決した。 yarn build時の環境変数読み込み deploy.ps1でバックエンド側のデプロイとクライアント側のソースビルド・デプロイを一括実行するようスクリプトを組む中で、タイトル部分の挙動が手動実行時と違うことを検知した。 もともとスクリプトを組む前はこちらのサイトを参考にproduction環境での環境変数をenv.productionに定義していたが、スクリプト上で.env.productionファイルを作成&yarn buildを実行するとうまくenv.productionファイルを読み込まないことが判明。 以下のように.env.productionファイルを使わず、直接環境変数に設定することで解決しました。おそらく権限周りの問題なのだと思われるが、よくわからず・・・。 #deploy.ps1抜粋 $env:REACT_APP_API_URL_PROD = aws cloudformation describe-stacks --output text --stack-name feed-notify-dev --query 'Stacks[].Outputs[?OutputKey==`ServiceEndpoint`].[OutputValue]' yarn build Write-Output "Client App Built" 終わりに 今回、初めてServerless Frameworkで本格的にアプリ構築をしてみましたが、いろいろとハマってしまって難しいと感じた面もありつつ、Serverless Frameworkそのものやプラグインがとても便利で、非常に勉強になりつつ楽しかったです。 ReactによるUI構築も勉強になったので、今回の経験を踏まえて、また新しいアプリ構築にチャレンジしてみたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【electron】electron+typescript+reactでデスクトップアプリを作ってみる

初書:2021/07/14→2021/11/25 mac : 11.4 npm: 8.1.0 node: 16.13.0 electron: 15.3.0 前書き 久しぶりにelectronを使うので、初期の設定から最低限の起動までをメモしておく。 最近Typescriptをよく使うので、せっかくならTypescriptと、あとReactも使えるみたいなのでまとめて使ってみる 参考サイト 今回こちらの方をかなり参考にさせてもらった。 Electron + TypeScript + React の環境構築 (Summer 2021) 使うモジュール一覧 ElectronはNode.jsで動くので、npmを使う。 そしてその時に色々と使うので、簡単に仕様などをメモしておく Electron JavaScript, HTML, CSS でクロスプラットフォームなデスクトップアプリ開発 が出来るオープンソフトウェアフレームワーク。 今回のベースとなるので詳しい内容は既にご存知でしょう(丸投げ)。 Electron | JavaScript, HTML, CSSによるクロスプラットフォームなデスクトップアプリ開発 Typescript Javascriptの型を厳密にした言語。コンパイルでjavascriptになるのでベースはjs。 適切に使うと型の恩恵を受けれる他、入力補完も出来るようになるので便利。 React ユーザインタフェース構築のためのJavaScriptライブラリ。今回はレンダラープロセス側で使う。 Reactにはクラスベースと関数ベースがあり、最近は関数ベースが主流になりつつあるが、 個人的にはクラスベースの方がやっていることが可視化しやすい感じがしてクラスベースを使っている。 下のコードもクラスベースなので、関数ベースで書きたい人は適宜書き換えてください。 webpack モジュールバンドラー。今回は複数のファイルをまとめて1つのファイルにしたり、コンパイル周りをしたりするために使う。 npm-run-all npmで複数のコマンドを連続で実行するためのもの。普段は&&や|を使っていたが、windowsでは使えない?らしい。 何かその辺をうまくやってくれるモジュール。 rimraf rm -rfをするもの。これもwindows関連でうまくやってくれるやつ。 今回はコンパイル先で不具合を起こさないように毎回消すために使う。 cross-env 実行時に任意の環境変数を設定できる。 インストール まずはディレクトリを作成し、その中でnpm init -y(-yはデフォルト値で決めてくれる) electronのインストール % npm install --save-dev electron typescriptのインストール % npm install --save-dev typescript ts-node @types/node ts-nodeはtypescriptのまま実行できるライブラリ Reactのインストール % npm install -save react react-dom % npm install --save-dev @types/react @types/react-dom webpackのインストール % npm install --save-dev webpack webpack-cli webpack-cliはwebpackコマンドを実行できるようにするライブラリ webpackでバンドルする時に必要なものをインストール % npm install --save-dev ts-loader css-loader mini-css-extract-plugin html-webpack-plugin @types/mini-css-extract-plugin ts、html、cssをそれぞれバンドルする時に必要 バンドルから実行に必要なものをインストール % npm install --save-dev rimraf cross-env npm-run-all 以上。ちなみにこの上にeslintを入れる予定だったのだが、想像以上に衝突が起こってしまい1、例外を作っていくのが大変だったので導入を断念した。 各種設定 無事にインストールが終われば各種設定を行う。gitとフォーマッタの設定は各自で。 Typescript % npx tsc --init これでtsconfig.jsonが出来たら、中身を少し弄る。 "target": "es2020", "lib": ["DOM", "ES2020"], "jsx": "react-jsx", "sourceMap": true, "outDir": "./dist", libに関しては必要なのかいまいち分かってない。targetと同じでdomだけなので不要な気もしている。 Webpack コードは参考サイトからほとんどお借りしている。 webpack.config.ts import path from 'path'; /** エディタで補完を効かせるために型定義をインポート */ import { Configuration, DefinePlugin } from 'webpack'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; const isDev = process.env.NODE_ENV === 'development'; /** 共通設定 */ const base: Configuration = { mode: isDev ? 'development' : 'production', // メインプロセスで __dirname でパスを取得できるようにする node: { __dirname: false, __filename: false, }, resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'], }, output: { // バンドルファイルの出力先(ここではプロジェクト直下の 'dist' ディレクトリ) path: path.resolve(__dirname, 'dist'), // webpack@5.x + electron では必須の設定 publicPath: './', filename: '[name].js', // 画像などのアセットは 'images' フォルダへ配置する assetModuleFilename: 'images/[name][ext]', }, module: { rules: [ { /** * 拡張子 '.ts' または '.tsx' (正規表現)のファイルを 'ts-loader' で処理 * node_modules ディレクトリは除外する */ test: /\.tsx?$/, exclude: /node_modules/, use: 'ts-loader', }, { /** 拡張子 '.css' (正規表現)のファイル */ test: /\.css$/, /** use 配列に指定したローダーは *最後尾から* 順に適用される */ use: [ /* セキュリティ対策のため(後述)style-loader は使用しない */ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: isDev }, }, ], }, { /** 画像やフォントなどのアセット類 */ test: /\.(bmp|ico|gif|jpe?g|png|svg|ttf|eot|woff?2?)$/, /** アセット類も同様に asset/inline は使用しない */ /** なお、webpack@5.x では file-loader or url-loader は不要になった */ type: 'asset/resource', }, ], }, /** * developmentモードではソースマップを付ける * * レンダラープロセスでは development モード時に * ソースマップがないと electron のデベロッパーコンソールに * 'Uncaught EvalError' が表示されてしまうことに注意 */ devtool: isDev ? 'inline-source-map' : false, plugins : [ new DefinePlugin({ 'process.env.VERSION_ENV': `"${require('./package.json').version}"`, }) ] }; // メインプロセス用の設定 const main: Configuration = { // 共通設定の読み込み ...base, target: 'electron-main', entry: { main: './src/main.ts', }, }; // プリロード・スクリプト用の設定 const preload: Configuration = { ...base, target: 'electron-preload', entry: { preload: './src/preload.ts', }, }; // レンダラープロセス用の設定 const renderer: Configuration = { ...base, // セキュリティ対策として 'electron-renderer' ターゲットは使用しない target: 'web', entry: { renderer: './src/renderer.tsx', }, plugins: [ /** * バンドルしたJSファイルを <script></script> タグとして差し込んだ * HTMLファイルを出力するプラグイン */ new HtmlWebpackPlugin({ template: './src/index.html', minify: !isDev, inject: 'body', filename: 'index.html', scriptLoading: 'blocking', }), new MiniCssExtractPlugin(), ], }; /** * メイン,プリロード,レンダラーそれぞれの設定を * 配列に入れてエクスポート */ export default [main, preload, renderer]; コンパイルの中にcssをパックするファイルがあるが、 tsxの中でcssをimportして使っているので不要なのかもしれない。 この辺りはwebpackやcss-importをもう少し理解して換える必要があると思われ。 package.json 実行コマンドを追加しておく。 またメインプロセスをdist/main.jsに変更しておく package.json "scripts": { "start": "run-s clean build serve", "clean": "rimraf dist", "build": "cross-env NODE_ENV=\"development\" webpack --progress", "serve": "electron ." }, "main": "dist/main.js", cleanはdistディレクトリとその中身を削除するコマンド buildはwebpackを使用してコンパイルするコマンド serveはElectronを起動するコマンド でstartがそれらを順番にまとめて実行するコマンド ちなみに(余談) buildコマンドにて、cross-envでNODE_ENV="development"を指定して、build後のコードを見てみると、 process.env.NODE_ENV === "development"がtrueに置き換わっているのが分かる。 これは、cross-envで指定したNODE_ENVの値を反映して置き換わっているのではなく、 webpackの仕様によって書き変わっている。2 他にprocess.envで指定した環境情報を置き換える場合は、 base.pluginsで使用しているDefinePluginというライブラリで同様に置き換えることができる。 このコードではVERSION_ENVをpackage.jsonのversionになるよう上書きしている。 各種ファイル作成 とりあえず実行に必要なファイルを作成する。 srcディレクトリを作成 src/main.tsの作成(メインプロセス) src/index.htmlの作成(レンダラープロセスのhtml部分) src/preload.tsの作成(プレロード) src/renderer.tsxの作成(レンダラープロセス) ファイルの中身はとりあえず空でいい。 とりあえず実行してみる % npm run start いくつかのコマンドが実行されたのち、electron .が実行されれば成功。 なお画面表示部分を作成していないので、ウィンドウは特に出てこない。 最低限のコードを書く 流石に何も表示されないのは面白くないので、よくあるHello worldを表示してみる。 main.ts 呼び出しの大元となるmain.ts。ここでレンダラープロセスを生成する。 src/main.ts import path from 'path'; import { app, BrowserWindow } from 'electron'; /** * BrowserWindowインスタンスを作成する関数 */ const createWindow = () => { const mainWindow = new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), }, }); // 開発時にはデベロッパーツールを開く if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools({ mode: 'detach' }); } // レンダラープロセスをロード mainWindow.loadFile('dist/index.html'); }; /** * アプリを起動する準備が完了したら BrowserWindow インスタンスを作成し、 * レンダラープロセス(index.htmlとそこから呼ばれるスクリプト)を * ロードする */ app.whenReady().then(async () => { // BrowserWindow インスタンスを作成 createWindow(); }); // すべてのウィンドウが閉じられたらアプリを終了する app.once('window-all-closed', () => app.quit()); 簡単にコード解説 createWindowはアロー関数で、new BrowserWindowで新しいウィンドウを生成している。 webPreferencesで指定している各種設定内容は後に少し出てくる。 mainWindow.webContents.openDevToolsではchromeのF12でよくみるあれを初めから表示している。 ちなみに本番では表示しないようになっているが、メニューバーに「toggle Developer tools」がある限り、普通に開くことができるので 非表示にしたい場合は注意。 app.whenReady()で準備ができれば先程の関数を呼び出し、ウィンドウを生成する。 app.once('window-all-closed', () => void);は、全てのウィンドウが閉じた時にAppを終了するイベント。 windowsではこれがなくても特に問題がないが、macの場合はウィンドウを閉じただけではAppが終了しない。(メールやメモなど、デフォルトのAppで見かける) そのため、ウィンドウが閉じると同時に終了させたい場合はこの一行が必要になる。 index.html src/index.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- CSP の設定 --> <meta http-equiv="Content-Security-Policy" content="default-src 'self';" /> <title>Electron Title</title> </head> <body> <!-- react コンポーネントのマウントポイント --> <div id="root"></div> </body> </html> 簡単にコード解説 metaタグでContent-Security-Policyを設定している。 これにより、埋め込みスクリプトや外部サーバーのスクリプト等を実行できなくなる。 これは安全に使うために入れているので、極力入れておいた方がいいが、外部ファイルの読み込み等は難しくなる。 body内には<div id="root"></div>だけが存在している。 この中にReactを用いたhtmlを構成していくので、この中は今は空でいい。 また、webpackにより、このしたの行にrenderer.jsが自動的に書き加えられる。 preload.ts 今現在はメインとレンダラーで通信を行わないので空ファイル renderer.tsx src/renderer.tsx import React from "react"; import ReactDOM from "react-dom"; class RootDiv extends React.Component<{}, {}> { constructor(props: {}) { super(props); } render = () => { return <>Hello World</>; }; } ReactDOM.render(<RootDiv />, document.getElementById("root")); 簡単にコード解説 Reactを使用しているので、React自体の詳細な説明は他のページを当たって欲しい。 今回はただ単にHello Worldを表示できればいいので、renderでは要素名なしの<>Hello World</>を返している。 また、React.Component内の関数は、constructorを除いて基本的にアロー関数を使用する。 理由はthisをbindすることを忘れる/bindが面倒だから。 再び実行してみる もう一度実行する。 % npm run start 今回はウィンドウが表示され、「Hello World」が表示されれば成功。 メインプロセスと通信する これで一応最低限Electronを作成出来たが、デスクトップアプリケーションなので、 基本的にメインプロセスと通信する必要が出てくる。 なので、今回はファイル名を入力して、存在するかチェックするとても簡単なアプリケーションを作成してみる。 方針決定 Electronなのでnodeを使えるように思えるが、main.tsのnew BrowserWindowの際に、 nodeIntegrationをfalseに設定していると、レンダラープロセス側からnodeにアクセスできなくなる。 これをtrueにしてしまえばレンダラープロセスだけで完結するが、例えばiframeや、aタグを踏んだりするなどして 外部のサイトを開くことが出来るようになると、そのサイトに悪意のコードがあればnodeを実行できるようになり、危険になる。 そのため、基本的にはnodeIntegrationはfalseで扱う。 ではどのようにメインプロセスと通信するかというと、先ほど空白にしたpreload.tsを経由して通信を行う。 プリロードスクリプトは、ウェブコンテンツの読み込み開始前にレンダラープロセス内で実行されるコードです。 これらのスクリプトはレンダラーのコンテキスト内で実行されますが、Node.js の API にアクセスできるようにより多くの権限が与えられています。 (中略) プリロードスクリプトは、グローバルな Window インターフェイスをレンダラーと共有し Node.js の API にアクセスすることができます。そのため、window グローバルに任意の API を公開してウェブコンテンツが利用できるようにすることで、レンダラーを強化する役割を果たしています。 プリロードスクリプトはアタッチされているレンダラーと window グローバルを共有しますが、contextIsolation のデフォルト値によりプリロードスクリプトの変数は window に直接アタッチできません。 プロセスモデル | Electronより つまり、contextBridgeを利用してレンダラープロセス側のグローバル変数に追加でき、かつnodeを実行できるレンダラープロセス側のソースコードになる。3 これを利用して、メインプロセスと繋ぐ橋を生成し、通信を行う。 preload.ts preload.ts import { contextBridge, ipcRenderer } from "electron"; export const preloadObject = { existFile: async (filePath: string) : Promise<boolean> => { const result = await ipcRenderer.invoke('existFile', filePath); return result; }, }; contextBridge.exposeInMainWorld('api', preloadObject); メインプロセスにはcontextBridge.exposeInMainWorldを利用することで使用できる。 これの第一引数で渡すstringがwindowに追加され、第二引数がその中に入る。 今回であれば第一引数がapiで、第二引数が{existFile: Function}なので、 window.api.existFiles(...arg)でレンダラープロセスで呼び出すことができる。 またexistFileの中ではipcRenderer.invokeを利用することでメインプロセス側に通信を試みる。 第一引数がイベント名で第二引数以降が渡す変数となる。 メインプロセス側の受け取りは後で記述する。 global.d.ts 唐突に出てきたd.ts。別に単にtsでもいいけど。 preloadからrendererに公開されたapiは、typescriptでは認識してくれないため型の推論をしてくれない。 そのため、その中間となる型定義ファイルを用意することで、typescriptの恩恵を受けようというもの。 ちなみに、preloadとmain間の通信で型推論する方法はないと思う。あれば教えてください。 ということで、srcの中に@typesディレクトリを生成し、その中にglobal.d.tsを生成する。 ちなみにこのglobalという名前は何でもいい。 src/@types/global.d.ts import { preloadObject } from '../preload'; declare global { interface Window { api: typeof preloadObject; } } これで、window.api.と入力するとexistFileが入力予測で表示される。 renderer.ts 先ほど使ったglobal.d.tsを使って型推論をしてもらう。 もちろんここからコピペするだけならanyでも何でもいいので使う意味はないが、この先は自身で作成すると思うので。 src/renderer.ts import React from "react"; import ReactDOM from "react-dom"; interface States { filename: string; } class RootDiv extends React.Component<{}, States> { constructor(props: {}) { super(props); this.state = { filename: "", }; } handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => { this.setState({ filename: event.target.value, }); }; handleOnClick = async () => { const { filename } = this.state; const result = await window.api.existFile(filename); alert(`${filename}は${result ? "存在しています" : "存在していません"}`); }; render = () => { return ( <> <input type="text" onChange={this.handleOnChange} /> <button onClick={this.handleOnClick}>検索</button> </> ); }; } ReactDOM.render(<RootDiv />, document.getElementById("root")); 簡単にコード解説 RootDivクラスの中身を大幅に書き換えた。 今回は結果をalertで表示するため、renderは入力boxとボタンのみ設置されている。 また、handleOnChangeとhandleOnClickはそれぞれtextの中身を書き換えた時と、ボタンをクリックした際に呼び出される。 これにより、stateのfilenameで入力情報を管理し、検索を押された際は先程のAPIを利用してファイルが存在するかどうかをチェックしている。 main.ts main.ts import fs from "fs"; import { app, BrowserWindow, ipcMain } from "electron"; // 先ほどのウィンドウ生成コードがここに入る ipcMain.handle("existFile", (event: Electron.IpcMainInvokeEvent, filename: string) => { return fs.existsSync(filename); } ); レンダラープロセス側からのイベントを受け取るには、ipcMain.handleを使用し、第一引数はipcRenderer.invokeの第一引数と一致している必要がある。 ipcMain.handleの第二引数は関数が入り、この関数の第一引数はElectron.IpcMainInvokeEventが入る。第二引数以降はinvokeの第二引数以降。 今回は、nodeの標準にあるfsのexistsSyncを使用して存在確認をしている。 再度実行してみる もう一度実行する。 % npm run start 今回はウィンドウに入力boxと検索ボタンが表示される。 試しに./src/main.tsと入力し、「./src/main.tsは存在しています」と表示されれば成功。 また、./main.tsと入力し、「./main.tsは存在していません」と表示されれば成功。 ちなみに相対パス指定で実行しているのは、nodeが提供するfs.existsSyncに依存しているため、npm run startを叩く位置によってパスは変化する。 終わりに とりあえずこれで最低限動作するようなところまで出来るようになるはず。 一部まだ見直さないといけなさそうなところもあるので、気が向いたら更新するかも。 まずElectronをsave-devで入れている時点で警告が出る。 ↩ ではcross-envは何をしているの?というと、webpack.config.tsのenvの中身を置き換えている。 ↩ ということはcontextBridgeを利用せずプレロードでnodeを呼び出せばいいのでは?誰もやらないということは多分何かあるのだと思うが…。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material UI のTextField とReact Hook Form の組み合わせでフォームのひらがなをカタカナに変換

フォーム入力でひらがなでの入力をカタカナに自動変換する機能をMaterial UI のTextField とReact Hook Form のバリデーションを組み合わせて実装したかったのでしてみました。 ひらがなをカタカナに変換する機能部分実装 カタカナとひらがなの UTF-16 コードポイントの値を比較すると、0x60 だけずれているとのことで、0x60 という値を、ひらがなに足せばカタカナに、カタカナから引けばひらがなに変換することができるとのこと。 export const hiraToKata = (string: string): string => { return string.replace(/[\u3041-\u3096]/g, (ch: string) => String.fromCharCode(ch.charCodeAt(0) + 0x60), ); }; 参考:文字列内のひらがなとカタカナを変換する フォーム部分の実装 Material UI やReact Hook Form のドキュメントを行き来しながらなんとか実装できました。 <TextField/> のonBlur に入力された値をカタカナに変換してsetValue してあげるとフォームからフォーカスが外れたときにカタカナに変換できるようになりました。 import { TextField } from '@mui/material'; import { Controller, useForm } from 'react-hook-form'; import { hiraToKata } from 'util/hiraToKata'; export const CustomForm = () => { const { formState: { errors }, control, setValue, } = useForm({ mode: 'onBlur', criteriaMode: 'all', }); return ( <Controller name='lastName_kana' control={control} rules={{ required: '入力が必要です' }} defaultValue='' render={({ field }) => ( <TextField {...field} variant='outlined' label='セイ' error={errors?.lastName_kana && true} helperText={errors?.lastName_kana && errors?.lastName_kana.message} onBlur={(e) => setValue('lastName_kana', hiraToKata(e.target.value))} /> )} /> ); }; React Hook Form であれこれ詰まってますが何とかかんとかやってます。 もっといい方法がありましたらぜひぜひご教授ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React Native]TextInputのサイズを自動調整する方法

はじめに こんにちは。今週もReact Native開発のノウハウを投稿させていただきます。 Webページに入力欄を作るとき、TextInputがよく使われています。入力された文字に応じて、TextInputのサイズを調整したい、という仕様もよくあるのですが、React Nativeでそれを実現するのは思ったより難しい、と私が気づいています。 ちなみに、React Native PaperにもTextInputがありますが、スタイルをつけているため、カスタマイズが必要な場合、React NativeのTextInputのほうが使いやすいと考えております。 調べたところ、TextInputのpropsにonContentSizeChangeを設定したら、高さの自動調整ができるけれど、デメリットが二つあります。 onContentSizeChangeでは、改行する時発火されるが、行を削除する時発火されない、というバグがある 高さが改行に応じて変更されるが、広さの変更ができない となると、onContentSizeChangeでは希望に応えられないときが多いではないかと思っております。ほかに良い方法を考えなければならないですね。 結論は最初に出す TextInputの後ろにTextを重ねていれば、自動調整ができるようになります。 もちろん、Textコンテンツの末尾にZero-width-spaceを忘れないように。 解決策 「Talk is cheap」、とにかくソースコードを見てみましょう。 // ... <View> <Text style={{fontSize: 18}}> {message}&#8203; </Text> <TextInput style={styles.msgInput} placeholder={''} onChangeText={text => setMessage(text)} value={message} multiline={true} numberOfLines={1} /> </View> // ... const styles = StyleSheet.create({ msgInput: { position: 'absolute', top: 0, left: 0, fontSize: 18, width: '100%', height: '100%', overflow: 'hidden' } }); ご覧の通り、TextInputに文字を入力したら、その後ろのTextにも表示されます。こうして、TextInputのサイズもTextと同じようになります。そして、Textのサイズは中に入れている文字次第のため、したがってTextInputのサイズも入力した文字に応じて自動調整ができるようになります。 Zero-width-spaceについて Textコンテンツの末尾に、&#8203をつけているのですが、これはいったい何でしょうか、と疑問を持っている方もいらっしゃると思いますね。 これはzero-width-spaceというものです。つまり、広さが0の文字のことです。 では、zero-width-spaceは何のためにここに入れているのでしょうか? Zero-width-spaceをつけるのは、改行を対応するためです。 この文字をつけない場合、どうなるか見てみましょう。 ご覧の通り、改行しても、何らかの文字を入力しない限り、高さが変更されなかったです。 Zero-width-spaceをつけましょう。 Zero-width-spaceの広さがゼロですが、一つの文字として高さがあります。そのため、改行したらTextの高さも変更されます。 こうして、入力文字に応じてTextInputのサイズを調整することができます。 最後に この記事はあくまでも私個人(初心者として)の経験です。間違っているところ、もっと良い方法がございましたら、ぜひ教えてください。 参考資料 https://github.com/necolas/react-native-web/issues/2160 https://www.yuzutalk.net/ ←このサイトは資料ではないけれど、同じくTextとTextInputを重ねていて、私にヒントを与えました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Reactにおけるメモ化に関して(useCallback / useMemo / React.memo)

どうも、@ちーずです。 アドベントカレンダー5日目、本日のテーマはReactにおけるメモ化に関してです!! そもそもメモ化とは? プログラムの高速化のための最適化技法の一種であり、サブルーチン呼び出しの結果を後で再利用するために保持し、そのサブルーチン(関数)の呼び出し毎の再計算を防ぐ手法である。 メモ化された関数は、以前の呼び出しの際の結果をそのときの引数と共に記憶しておき、後で同じ引数で呼び出されたとき、計算せずにその格納されている結果を返す。 参考: メモ化 - Wikipedia つまり、計算結果をキャッシュし、それを再利用することをメモ化と言います。 なぜメモ化が必要? Reactでは下記のようなタイミングでComponentが再レンダリングされます。 state / props が更新された時 親コンポーネントが再レンダリングされた時 Componentが再レンダリングすると、 そのComponent内で定義している関数や変数も新しく生成されてしまい、 値が変わっていなかったとしても再計算されてしまいます。 Reactでは油断しているとそのような予期していない再計算が増えてしまい、 サイトのパフォーマンスが影響する可能性があるため、メモ化の重要度が高いです。 メモ化する時の注意点 軽い処理に対してメモ化すると、キャッシュを参照するオーバーヘッドが発生します。 また、メモ化の必要性はアプリケーションによっても異なります。 そのため、何でもかんでもメモ化するのではなく、実行してみてパフォーマンスがちゃんと改善したか確認しながらメモ化するとよりパフォーマンスの向上に繋がると思います。 (ちなみに自分はなんでもかんでもメモ化しちゃっていました...反省) Componentをメモ化 - React.memo React.memoは、Componentをメモ化するHOC(higher-order component) です。 Propsの値に変更がなかった際の再レンダリングを抑制することができます。 HOCとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数のことです。 書き方 // 第一引数: 関数Component export const heavyComponent = React.memo(() => ( // とても思い処理 <p>子Componentです</p> )); どんな時に使う? レンダリングコストが高いコンポーネント (そもそも、値のメモ化でどうにかなるケースの方が多そうですが...) Component自体が重い処理をもっている場合は、メモ化することでレンダリングを避けることができます。 頻繁に再レンダリングされてしまう可能性があるコンポーネント 親Componentの変更が頻繁に発生する場合、子コンポーネントがその度に不要な再レンダリングをしてしまうため React.memoでメモ化することおすすめします。 親Componentの処理に依存してComponentがのレンダリングが頻繁に起きてしまう場合、 そもそものComponent設計が正しくない可能性もあります。 そのような場合は、脳死メモ化するのではなく一度設計を見直した上でメモ化することをおすすめします。 値をメモ化 - useMemo useMemoは、実行結果や値をメモ化することができるフックです。 書き方 // 第一引数: コールバック関数 // 第二引数: 依存配列 - この値が変更された時のみ再計算 useMemo(() => function(), [deps]) どんな時に使う? 非常にコストのかかる計算を行う場合 非常にコストのかかるの定義は難しいですが、 下記記事をよんでみると、100回くらいの配列のループでuseMemoを使うのは意味がないかなぁ。。。と思いました。 ▼ 参考: メモ化していない時と 500回以上のループ処理並の複雑さの処理 何度も再レンダーされる可能性がある 時に使えるかなと思いました! 関数をメモ化 - useCallback useCallbackはメモ化されたコールバック関数を返すhooksです。 書き方 // 第一引数: コールバック関数 // 第二引数: 依存配列 const func = useCallback(() => { // callback関数 }, [deps]); どんな時に使う? 関数を返すカスタムhooks カスタムフックは、再利用性を高いhooksを定義するためのものであり、 Component側で呼ばれる際にその関数が再生成されることを利用者側が考えなくても良いように設計すべきです。 そのため、カスタムフックが関数を返すなら、基本的にメモ化することをお勧めします。 筆者は下記記事にものすごく感銘を受けたので、より詳しく知りたい人はぜひ読んでください!! ▼ 参考 メモ化されたComponentに関数を渡す場合 React.memoの復習になりますが、propsの値が同一である場合は再レンダリングは走りません。 しかし、そのまま関数をメモ化された子Componentに渡してしまうと 親が再レンダリングされたタイミングで関数が再生成されてしまうため、同一の値と判定されないため再レンダリングが走ります。 そのため、関数の同一性を保つためにuseCallbackで囲いましょう! 以上 「Reactにおけるメモ化に関して」 でした!! いつか、メモ化パフォーマンス改善実験をやってみたいな〜と思います!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ネットで知り合った仲間と農家の3代目の困りごとをFirebaseで解決した話

はじめに ※この記事はfreee APIで業務を楽しく便利にハックしよう!アドベントカレンダー2021の6日目の記事として投稿しています。 はじめまして。今年の1月からプログラミングの勉強をしているものです。 今回は、ネットで知り合った仲間とともに(顔もフルネームも知らないもの同士で)半年間、開発を行い、農家の3代目の困りごとを解決するアプリを作った話をしたいと思います。 きっかけ 私は技術力を身につけたいと思い、今年の1月〜4月ごろまで独学で勉強を進めていました。しかし次第に写経に飽き、このままUdemyなどで写経をして学習するより実際になにか動くものを作るほうが楽しいだろうなと思うようになりました。 また、せっかくなら1人でやるより誰かと一緒に開発する方がもっと楽しいだろうなと思い、勇気を出してネットで仲間を募り、アプリのチーム開発を始めてみることにしました。 募集したメンバーのうちの1人が農業をテーマになにか取り組みたいという希望があったため、農業の課題を解決するアプリを作ることにしました。 農家限定のSNS,直販のプラットフォームなど、いろんなアプリのアイデアが出ましたが、ど素人がいきなり、多くのユーザーがいる前提で価値がでるアプリを作るというのは、資金面でまあ難しいだろうということで、まずは目の前にいる1人を救えるアプリを作ろうという決断にいたりました。 目の前にいる「1人」を決める 農家のために作ると決めたものの、農家と関わりがあったのが私くらいしかいなかったため、「目の前にいる1人」の選定は私が大学時代に農業体験に毎年行っていた農家さん(専業農家)にすることになりました。 さっそく、「農家向けアプリを作りたいので、いいものができたら、使ってもらえないか」と打診したところ、合意が取れ、アプリ開発のためのヒアリングがスタートしました。 想定ユーザーの抱えている困りごとの洗い出し 2代目の社長から世代交代をして、頑張っているたくやさん(専業農家3代目30代男性)が、当時、抱えていたことは以下のようなことでした。 現在は直販のお客さんはみなFAXで注文してくるので、それをみて、注文内容を確認している。 以前は親戚のSEだった叔父が作ってくれたシステムがあったが、消費税の値上げに伴い使えなくなってしまった。 今、顧客を管理するものがないので、毎年くれるリピーターからの注文も、毎度宛名を手入力してヤマトの送り状を作成している。 注文や発送の一覧を管理しているものがないので、ヤマトの営業所に、野菜を持って行って、ヤマトの担当者が伝えてくれる確定情報をもとに、はじめて今日、自分が何件、誰に発送するのかを把握する。 請求管理は親父の担当で、通帳を渡してもらえないので、入金漏れがあったかどうかもわからない。 以前、会社が潰れそうになったときに、前職(食品メーカーの営業時代)の得意先のスーパーに何度も頭を下げて野菜を置かせてもらった。その得意先から入金があった時に、真っ先にお礼が言いたいのに、親父が銀行の通帳をもっていて渡してくれなかった。いつ入金があったのかわからなかった。だからすぐにお礼が言えなかった。それが今でも涙が出るほど一番悔しい。 と私に、教えてくれました。 アプリの要件を整理する たくやさんの悩みを解決するために、検討したアプリの要件は下記としました。 ユーザーはアプリで顧客を管理でき、freeeの取引先とも連携できる ユーザーはアプリで注文情報を管理でき、freeeの請求書とも連携できる ユーザーはアプリで受注〜発送〜請求〜入金までを管理でき、freeeの消し込み情報と連携できる ユーザーは登録した宛名情報をもとに、ヤマトで野菜を送る際に必要な送り状を作成できる ユーザーはヤマトの伝票情報をアプリに反映することで、配達状況を確認できる 技術選定 Next.jsをフル活用して、Vercelにデプロイすることも考えましたが、今回ははじめてのアプリ開発であったこともあり、freee社がテンプレートを出していたFirebaseを選択しました。 複雑になりがちな認証周りはFirebase Authのカスタムトークン認証とfreeeSDKを使い実装でき、バックエンドはすでにあるサンプルを参考に実装できたので、かなり効率的に開発が行えたかなと思います。 一方で下記の要望がチームにあったので、フロント部分はテンプレートを使わず自分たちで実装しました。 React/Next.jsを使って書いてみたい Firebase v9を使って軽量化してみたい useSWRを使ってfreeeAPIコールの最適化をしてみたい chakraUIを使いたい 400,401のエラーをハンドリングしたい Firestoreからのダイレクトな書き込み処理を行いたい Firestore rulesで権限を制御したい アプリ構成 アプリ構成は下記のブログを参考に、↓こんな感じで進めました。 なんとかできあがったアプリを納品 そんなこんなで、迎えた納品日。 夜中2時までみんなで頑張り最後まで直せるバグをひたすら直し、8/14にアプリを納品しました。喋ってる間に寝そうになりましたが、文化祭の前日みたいなノリでとっても楽しい時間でした!! 総勢、200件近いチケットを消化し、なんとか、繁忙期であるとうもろこしの収穫時期の前にアプリの納品を間に合わせることができました。 自分たちのアプリで業務がハックされた瞬間 そして、数日後、作ったアプリで管理されたお届け先情報をもとに、たくやさんが収穫したとうもろこしが各チームメンバーの家に届きました。 アプリを使って届いたとうもろこし。箱を見ただけで泣ける! しかも産地直送だから超うまい!!! 2人目のユーザーに価値を届けるために 我々の学習目標は見事、達成! たくやさんからも、「とても管理がしやすくなった。これ他の人にもぜひ提供したほうがいいよ!」というお褒めの言葉をもらうことができました。 その後チームのこれからについて、みなで検討した結果、2ndステップとして、アプリを世の中にリリースすることを目標に開発を再開することに決めました。 2ndステップではリリースを強く意識して、より高い品質にしていくためにQAエンジニアを置き、テストの自動化などに取り組んだり、さらにメンバーを募集して、10人体制での開発を行っていきました。 そして迎えた2021年12月。 今年の1月にhtml/cssを学びはじめた駆け出しエンジニア集団のアプリ開発のそのコミット数は3000、JIRAのチケット数は400枚を突破しました。 現在、何人かの現役エンジニアにレビューをしてもらっている最中で世にリリースできる日もそう遠くないかもしれません。 チーム開発での学び 開発プロセスとしては、JIRA・miro・Githubを使って、アジャイル開発で進めました。 スプリントは1週間単位とし、月〜金の朝7:00〜をデイリースクラム、土曜の朝7:00〜をレトロスペクティブ、日曜の朝7:00〜をスプリントプランニングとしました。 5月下旬から開発を開始し、毎日、バーンダウンチャートを追っかけながら進めましたが、かなり試行錯誤でありました。 なんたって全員がプログラミング初心者・ようやくReactでTodoアプリを模写できるレベル。 当時のバーンダウンチャートは驚くほどガタガタでした。笑 綺麗な右肩下がりのバーンダウンチャートを描けるチームになるために取り組んだことを3つほど紹介します。 各メンバー何かしらの役割を持つ 限られた人がチームを引っ張るのでなく、すべての領域で一から全員議論をするでもなく、各メンバーがそれぞれの役割(スクラムマスター・広報・チーム活性化・コード品質・デザインなど)を持ち、その領域の最終意思決定権を持つようにしました。これにより、限られた時間で議論が発散的にならず、集中して行えたように思います。 チームの決め事は定期的に見直す 当初のデイリースクラムは6:10からのスタートでした。これは、朝4時から勉強しているメンバーが2人、朝7時くらいまで起きていてそれから寝るメンバーが1人いたからです。 しかしその後、メンバーが入れ替わり、朝が苦手なメンバーが増えた時には、開催を22:00からにしました。一度決めたからといってそのままにせず、何事も今いるメンバーの最適解を模索することを心がけました。現在は、7:00に落ち着いています。 チームを活性化させ続けるために卒業制度(出戻り自由)をもうける ネット上のみで出来上がったこの脆く儚い関係は、オフラインの飲み会や学業・仕事の予定にはどうしても勝てません。チーム立ち上げ当初は、「今はちょっと忙しいけど落ち着いたら対応できます。」という言葉を信じ、忙しくなってしまったメンバーを待ち続けていました。が、待てど待てど戻ってこないメンバー。いつしか音信不通になってしまい、悲しい気持ちとともに、お任せしたタスクは宙ぶらりん。チームの士気も少し下がってしまいました。このままではチームが空中分解してしまうという危機感から、週5日以上(2日間は休んでOK)の出席を求めるチームガイドラインをもうけるとともに、2週連続でガイドラインを守れない場合には卒業を促し、幽霊部員を作らないようにしました。これにより、けじめもついて、チームとして卒業生を気持ちよく送り出すこともできるし、学習意欲の高いメンバーを新たに募集するということができるようになりチームのベロシティも維持できるようになりました。 これ以外にもチーム運営に苦戦することもいくつかありましたがその全てが学びの連続でした。また総勢19人のメンバーがこのチーム開発に関わり、この活動をきっかけに次々と仲間がエンジニアとしての転職に成功していく様子を目の当たりにし、チームを立ち上げた身として本当に嬉しい気持ちになり、色々な経験を提供してくれた仲間に本当に感謝の気持ちでいっぱいです。 最後に freeeAPIはリファレンスが充実していて開発環境もすぐに手に入るしAPIを叩くのを学ぶのに非常によかったと思います。(ヤマトは最後までB2やAPIを試せる開発環境を手に入れることができなかった...泣) しかし、現役エンジニアにとっては簡単なことでも私たち初学者にはフロントエンドの技術習得についてキャッチアップが大変な部分も多くありました。そんなとき、先人たちが残してくれたものが非常に役に立ちましたので、最後におすすめ講座一覧をご紹介して終わりたいと思います。 これからプログラミングを始めて、freeeAPIを使ったアプリ開発をしてみたいという方はぜひ参考にしてみていただけると幸いです。 おすすめ講座一覧 NO 講座名 1 VS Codeの使い方講座!日本語化する拡張機能のインストールも紹介!byしまぶーさん 2 無料かつ最速でプログラミングを学ぶ!ドットインストールの効率的な使い方! byしまぶーさん 3 Git:はじめてのGitとGitHub by清透さん 4 HTML講座 byしまぶーさん 5 CSS講座 byしまぶーさん 6 JavaScript講座 byしまぶーさん 7 モダンJavaScript講座 byしまぶーさん 8 モダンJavaScriptの基礎から始める挫折しないためのReact入門 byじゃけぇさん 9 React入門 byあべちゃん 10 今更解説するReactHooks入門講座 byあべちゃん 11 Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版 byじゃけぇさん 12 Firebase入門 by前田さん 13 Firebase入門 byあべちゃん 14 Firebase設計 bymogaさん 15 EsLint/Prettier ※サロン限定公開 byしまぶーさん 16 Next.js ※サロン限定公開 byしまぶーさん 17 TypeScript ※サロン限定公開 byしまぶーさん
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでcommit時にaddされているファイルにのみESLintとPrettierを実行する

何をするのか タイトル通りで、Next.jsでcommit時にステージング(git add)されているファイルにのみnext lint --fixとprettier --wrightを実行します。 ググれば日本語の記事でもやり方は沢山出てくるのですが、意外と落とし穴があったので今回はその詳細を書いていこうと思います。 ※大前提として初めからeslintが導入されている11以降のバージョンを使用しましょう。 よくあるやり方 huskyとlint-stagedを入れて.husky/_/pre-commitとpackage.jsonで以下のように設定して、commitする。 #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn lint-staged { "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint --dir src", "lint:fix": "next lint --fix --dir src", "format": "prettier --write ./src/**/*", "prepare": "husky install" }, "dependencies": { } "devDependencies": { }, "lint-staged": { "*.(js|ts)?(x)": [ "yarn format", "yarn lint:fix" ] } } これの何が問題かというと、これだと該当する全てのファイルにリントとフォーマットが効いてしまいます。 今回はステージングされているファイルにのみ反映したいのです。 lint-stagedという名前ですが、そこら辺をよしなにやってくれたりはしないようです。 やり方 「よくあるやり方」の問題点ですが、.lintstagedrc.jsを使ってコマンドを動的に生成する事で解決します。 ただその前にそこまでの一通りの手順を記載いたします。 また今回はJavaScript Standard Styleを適用したいと思います。(ここはお好みで何でも良いです) パッケージの導入 ※全部一気にやってもいいですが、今回はわかりやすく分けてインストールしています。 まずNext.jsのプロジェクトが作成し終わったら、そのディレクトリで、 yarn add --dev eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node を実行して、JavaScript Standard Styleに必要なパッケージをインストールします。 次にコードフォーマッターのprettierと、eslintとprettierのルールの競合を回避してくれるeslint-config-prettierをインストールします。 yarn add --dev prettier eslint-config-prettier そして最後にcommitやpushの前に特定の処理を挟んでくれるhuskyと、ステージングされているファイルの処理を行うためのlint-stagedをインストールします。 yarn add --dev husky lint-staged パッケージの設定 次にeslintとhuskyの設定を行います。 まずeslintrc.jsonを以下のように編集します。 { "extends": ["standard", "next", "prettier"] } このstandardとprettierはそれぞれJavaScript Standard Styleとeslint-config-prettierの設定です。 またこの時の注意点ですがスタイルのルールはnextより前に、フォーマッターのルールはnextより後に書きます。 次にhuskyの設定を行います。 以下のコマンドを実行してhuskyを初期化します。 npx husky-init && yarn ↑が実行されると、自動でルートディレクトリに.husky/_/pre-commitが作成されるので、そのファイルを以下のように書き換えます。 #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn lint-staged 最後にlint-stagedの設定を行います。 ルートディレクトリに.lintstagedrc.jsを作成して、以下の内容を書き込みます。 module.exports = { '*.(js|ts)?(x)': filenames => { const rawFilePaths = filenames.map(file => { const cwd = process.cwd() const isMatchingPathFormat = file.includes(process.cwd()) const myCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/') const rawFilePath = file.replace(myCwd, '.') return rawFilePath }) return [ `prettier --write ${rawFilePaths.join(' ')}`, `next lint --fix --file ${rawFilePaths.join(' --file ')}` ] } } 以上でインストール&設定は完了です。 これでgit commitすると、自動でステージングされているファイルにリントとフォーマットが効くようになりました。 .lintstagedrc.jsについて まず.lintstagedrc.jsが何をやっているのかですが、returnで返される配列のコマンドをcommitの前に実行してくれます。 つまり単純に module.exports = { '*.(js|ts)?(x)': filenames => [ 'yarn format', 'yarn lint:fix' ] } のように書くと、ステージングされたファイルにjs/jsx/ts/tsxが存在すると、yarn formatとyarn lintが実行されます。 ただしこれでは「よくあるやり方」で書いたように全てのファイルにリントとフォーマットが反映されてしまいます。 そのためステージングされているファイルにのみ反映するコマンドを引数filenamesを使用して生成する必要があります。 コード解説 このfilenamesですが、こいつはステージングされているファイルの絶対パスを列挙した配列であり、中身は以下のようになっています。 [ 'C:/Users/username/Desktop/my_project/.lintstagedrc.js', 'C:/Users/username/Desktop/my_project/next.config.js', 'C:/Users/username/Desktop/my_project/src/pages/members/index.tsx' ] この配列をprocess.cwd(ここではC:/Users/username/Desktop/my_projectが出力される)を使ってプロジェクトルートまでを.に置換します。 const rawFilePaths = filenames.map(file => { const cwd = process.cwd() const isMatchingPathFormat = file.includes(process.cwd()) const myCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/') const rawFilePath = file.replace(myCwd, '.') return rawFilePath }) [ './.lintstagedrc.js', './next.config.js', './src/pages/members/index.tsx' ] あとはこの配列を使ってお好みのコマンドが生成できます。 例えば上記のprettier --write ${rawFilePaths.join(' ')}であれば、prettier --write ./.lintstagedrc.js ./next.config.js ./src/pages/members/index.tsxとなります。 注意点 ※isMatchingPathFormatとmyCwdですが、windowsの場合process.cwd()の出力がC:\Users\username\Desktop\my_projectのようにスラッシュがバックスラッシュになってしまう事があるのでそれの調整をしています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでcommit時にaddされているファイルにのみリントとフォーマットを実行する

何をするのか タイトル通りで、Next.jsでcommit時にステージング(git add)されているファイルにのみnext lint --fixとprettier --wrightを実行します。 ググれば日本語の記事でもやり方は沢山出てくるのですが、意外と落とし穴があるので今回はその詳細を書いていこうと思います。 ※大前提として初めからeslintが導入されている11以降のバージョンを使用しましょう。 よくあるやり方 huskyとlint-stagedを入れて.husky/_/pre-commitとpackage.jsonで以下のように設定して、commitする。 #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn lint-staged { "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint --dir src", "lint:fix": "next lint --fix --dir src", "format": "prettier --write ./src/**/*", "prepare": "husky install" }, "dependencies": { } "devDependencies": { }, "lint-staged": { "*.(js|ts)?(x)": [ "yarn format", "yarn lint:fix" ] } } これの何が問題かというと、これだと該当する全てのファイルにリントとフォーマットが効いてしまいます。 今回はステージングされているファイルにのみ反映したいです。 やり方 「よくあるやり方」の問題点ですが、.lintstagedrc.jsを使ってコマンドを動的に生成する事で解決します。 ただその前にそこまでの一通りの手順を記載いたします。 また今回はJavaScript Standard Styleを適用したいと思います。(ここはお好みで何でも良いです) パッケージの導入 ※全部一気にやってもいいですが、今回はわかりやすく分けてインストールしています。 まずNext.jsのプロジェクトが作成し終わったら、そのディレクトリで、 yarn add --dev eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node を実行して、JavaScript Standard Styleに必要なパッケージをインストールします。 次にコードフォーマッターのprettierと、eslintとprettierのルールの競合を回避してくれるeslint-config-prettierをインストールします。 yarn add --dev prettier eslint-config-prettier そして最後にcommitやpushの前に特定の処理を挟んでくれるhuskyと、ステージングされているファイルの処理を行うためのlint-stagedをインストールします。 yarn add --dev husky lint-staged パッケージの設定 次にeslintとhuskyの設定を行います。 まずeslintrc.jsonを以下のように編集します。 { "extends": ["standard", "next", "prettier"] } このstandardとprettierはそれぞれJavaScript Standard Styleとeslint-config-prettierの設定です。 またこの時の注意点ですがスタイルのルールはnextより前に、フォーマッターのルールはnextより後に書きます。 次にhuskyの設定を行います。 以下のコマンドを実行してhuskyを初期化します。 npx husky-init && yarn ↑が実行されると、自動でルートディレクトリに.husky/_/pre-commitが作成されるので、そのファイルを以下のように書き換えます。 #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn lint-staged 最後にlint-stagedの設定を行います。 ルートディレクトリに.lintstagedrc.jsを作成して、以下の内容を書き込みます。 module.exports = { '*.(js|ts)?(x)': filenames => { const rawFilePaths = filenames.map(file => { const cwd = process.cwd() const isMatchingPathFormat = file.includes(process.cwd()) const myCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/') const rawFilePath = file.replace(myCwd, '.') return rawFilePath }) return [ `prettier --write ${rawFilePaths.join(' ')}`, `next lint --fix --file ${rawFilePaths.join(' --file ')}` ] } } 以上でインストール&設定は完了です。 これでgit commitすると、自動でステージングされているファイルにリントとフォーマットが効くようになりました。 .lintstagedrc.jsについて まず.lintstagedrc.jsが何をやっているのかですが、returnで返される配列のコマンドをcommitの前に実行してくれます。 つまり単純に module.exports = { '*.(js|ts)?(x)': filenames => [ 'yarn format', 'yarn lint:fix' ] } のように書くと、ステージングされたファイルにjs/jsx/ts/tsxが存在すると、yarn formatとyarn lintが実行されます。 ただしこれでは「よくあるやり方」で書いたように全てのファイルにリントとフォーマットが反映されてしまいます。 そのためステージングされているファイルにのみ反映するコマンドを引数filenamesを使用して生成する必要があります。 コード解説 このfilenamesですが、こいつはステージングされているファイルの絶対パスを列挙した配列であり、中身は以下のようになっています。 [ 'C:/Users/username/Desktop/my_project/.lintstagedrc.js', 'C:/Users/username/Desktop/my_project/next.config.js', 'C:/Users/username/Desktop/my_project/src/pages/members/index.tsx' ] この配列をprocess.cwd(ここではC:/Users/username/Desktop/my_projectが出力される)を使ってプロジェクトルートまでを.に置換します。 const rawFilePaths = filenames.map(file => { const cwd = process.cwd() const isMatchingPathFormat = file.includes(process.cwd()) const myCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/') const rawFilePath = file.replace(myCwd, '.') return rawFilePath }) [ './.lintstagedrc.js', './next.config.js', './src/pages/members/index.tsx' ] あとはこの配列を使ってお好みのコマンドが生成できます。 例えば上記のprettier --write ${rawFilePaths.join(' ')}であれば、prettier --write ./.lintstagedrc.js ./next.config.js ./src/pages/members/index.tsxとなります。 注意点 ※isMatchingPathFormatとmyCwdですが、windowsの場合process.cwd()の出力がC:\Users\username\Desktop\my_projectのようにスラッシュがバックスラッシュになってしまう事があるのでそれの調整をしています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む