- 投稿日:2019-02-11T22:26:04+09:00
Reactアプリの本番用ビルド(production)でお手軽にconsole.log()の出力を止める
概要
本番環境ではコンソールに不要なログは出力したくないものです。
create-react-appではその手段を提供していないため、自身でやる必要があります。
webpackの設定を使用する方法はあるようですが、create-react-appでは設定ファイルへアクセスするにはイジェクトが必要になります。
そこでイジェクトすることなくお手軽にconsole.log()の出力だけ止めてしまおうという話です。読者対象
- Reactアプリの本番環境で不要なログ出力で困っている。
- だけどcreate-react-appは絶対にイジェクトしなくない。
- consoleのラッパー作ってそれですべて置き換え、中で切り替えるのは面倒。
- ログの内容は仮に見られたとしても問題なく、パフォーマンス低下を防ぐために止めたいだけ。
Road to disable console.log()
console.log()を冒頭ですり替えちゃおう(;´∀`)
create-react-appでプロジェクト作るとsrc/index.jsがあると思います。
このソースはMatarial-UIのサンプルなのでちょっと違うかもしれませんが、要はrender()の前に実行すればOK。
render()の後だとちょっと出力が見られました。index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import Index from './pages/index'; import * as serviceWorker from './serviceWorker'; // ↓ この一行でnpm run buildの際にconsole.log()を何もしないメソッドにすり替え process.env.NODE_ENV !== "development" && (console.log = () => {}); ReactDOM.render(<Index />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA serviceWorker.unregister();しっかりif文でやるとこう。
index.jsif (process.env.NODE_ENV !== "development") { console.log = () => {}; }
npm run build
実行時にはprocess.env.NODE_ENVにはproductionという文字列が入ってきますが、それがもし変更された際への考慮として逆のdevelopmentじゃない場合を条件にしています。
これなら変更あった際、開発段階でログ出力されなくなることから、変更にすぐ気が付くので(;^ω^)
一時的に出力されることに抵抗がないなら、if (process.env.NODE_ENV === "production")
が直感的でいいかもしれません。お手軽といったのには訳がある・・・
もう終了と思ったかもしれませんが注意点があります。
なぜなら、クライアント環境でこれを簡単に戻せる方法があるからです。
ブラウザのデベロッパーツールのコンソール開いて以下を打ち込みます。consoleconsole.log = console.info
そしてログ出力されるようなアクションを実行してみてください。
ログ出力が復帰します( ゚д゚)ポカーン
じゃあinfoもつぶせばOKかというとNoです。
consoleにはlog,debug,info,warn,error等ありますので、気になるならその辺もつぶしましょう(;^_^A
試しにとある人気サイトをのぞいてみると、これらのメソッドがすべて置き換わっていました。すり替えのその先の可能性
まだ試してはいないですが、errorの場合にはエラー内容を開発元にフィードバックできそうだなと思いました。
メトリックツール導入していると、機能の一つとしてエラーの際の内容を送ってくれる製品がありますが、あれを自前でやるイメージです。
どのブラウザでどういうエラーが起こったのか把握できるため、開発環境ではわからなかったバグFixに非常に重宝しました。
もしやる機会があってうまくいったら記事にしてみようかと思います。
ツール導入してやることはないかもしれないですが(;^_^Aおまけ
後でググったところ、1年以上前ですが本家のissueに質問した方がいたようでした。
回答の一つして、同様に空のFunctionですり替え提案されてました(;^_^A
https://github.com/facebook/create-react-app/issues/1491
- 投稿日:2019-02-11T21:16:18+09:00
Electron + React(v16.8) で動画・静止画ビューワーを作った
※本記事には少しだけR18要素がありますので、拒絶反応がある方はページを閉じることをおすすめします。
以前、shokushuという動画プレイヤーをElectronで作った のですが、静止画も扱える shokushu2 というアプリケーションをElectronで作りました。
https://github.com/y-takey/shokushu2
趣味アプリなので、実行ファイルの配布はしていません。
ご興味あればgit clone
してからyarn install && yarn package
すると、<PROJECT_ROOT>/release
ディレクトリに ビルド環境に適応した実行ファイルが作成されます。
ただし Mac でしか動作確認をしていないため Windows 等で動くかは不明です。特にアプリケーションの性質上、ファイルシステムにアクセスするのでその部分が微妙です。
詳細は README を参照してください。モチベーション
- 前出の動画プレイヤーを数年使っていて、改善したい部分が溜まっていた。
- 原点回帰とでもいうか、近年は静止画を購入することも多くなっていたが、気にいるビューワーがなかった。K○miflo(R18のサービスなので、伏せ字および無リンクにしておきます)と同じように閲覧したかった。
- 動画も静止画も同じようにプライベートなプラットフォーム上で一元管理したかった。
- React Hooks が実戦投入可能か評価したかった。(2019/2月上旬に React 16.8 の安定版がリリースされたが、実装開始時はまだ alpha 版だった)
使用した主な技術・ライブラリ
Electron
HTML, JS, CSS 等のWeb技術でクロスプラットフォームなデスクトップアプリケーションが作れるやつ。
現在ではNW.jsや neutrino等の選択肢もあるが、こだわりも不満も特になかったので今回もElectronを採用。
ちなみに、このボイラープレートを使って、初期セットアップを済ませた。React v16.8
今回の主目的の一つであるHooks の評価のために、16.8 のalpha バージョンから使用。
ちなみに Redux 等は使用しておらず、React 標準機能の Context 等を使用。(とりまRedux、という風潮(?)が嫌い)
Ant Design
中国のアリババのグループ企業であるAlipayのエンジニアが主に開発しているUIライブラリ。
今までUIライブラリは色々試してきたけど、デザイン・コンポーネント数・扱いやすさ等の総合評価では1番いい。
ちなみに名前の由来は、アリ(ババ|ペイ)→蟻(日本語)→Ant(英語)?NeDB
組込のドキュメント型データベース(NoSQL)。API は MongoDB のサブセットなので、MongoDB を使ったことがあればほとんど同じ感じで使える。 MongoDB を使ったことがなくても 何かしら O/Rマッパー を使ったことがあれば雰囲気で使えるはず。
データファイルはプレーンテキストなので、直接ファイルの中身を参照・編集できて嬉しい。React Hotkeys
ホットキー(ショートカットキー)用のライブラリ。同種のライブラリを色々試した中で一番フィットした。
ちなみにreact-hot-keys
等、似た名前のライブラリが多いので注意。
2019/02/11 時点での安定版は v1 だが、v2 が開発中で、使用したい機能がv2からだったのでv2を使用。開発中とは言え、特にバグに遭遇することもなく安定していた。機能
リスト
- 一般的な検索・ソート・ページングができる
- サムネイルは静止画の場合は最初のページを使用。動画の場合は再生時間の中央のイメージを使用。ただしブックマーク(後述)が登録されている場合はその最初の位置を使用。
ビューワ
静止画ビューワ
- 画像からは分かりにくいけど、紙の本と同じように2ページ分を左右に並べて表示。(設定で1ページにもできる)
- 普通の動画プレイヤーと同じ操作感で← , → でページ送りができる。通常は2ページ単位で移動するが、 shift+← , shift+→ で1ページ単位になる。(見開きページ用の調整)
- b でブックマークを設定できる。(スライダー上の白丸がブックマーク位置)同じ位置でまた bを押すとブックマークを解除。
- ↑, ↓で現在ページを起点に前後のブックマークへ移動。
動画ビューワ
- 機能的には静止画ビューワとほぼ同じ。
- ←, → での移動秒数は設定可能。(初期値は10秒)
メタデータの編集
- リストまたはビューワからタイトルや作者・タグ等のメタデータを編集できる
- タイトルを変更すると、実際のファイル名・ディレクトリ名も変更される。
- ちなみに動画・静止画の最後まで閲覧すると視聴回数を自動インクリメントするため、編集項目に視聴回数(Viewed Count)は不要なはずだけど、開発時にインクリメントしすぎるバグがあり、回数を補正するために編集可能にした。今は当該バグはないけど、その名残。
- 投稿日:2019-02-11T18:13:27+09:00
[React] モーダルの作り方
概要
今回はモーダルの作り方についてみていきます。
モーダルは項目を削除する時とかに出てくる画面のことで、「本当に削除しますか?」に対して「はい」、「いいえ」みたいに選択できるものです。ポータル
モーダルは画面いっぱいを占領するので他の要素よりもトップに来なければいけません。
そこでポータルについて意識しないといけません。
簡単にいうと構造的に深くに作ってはダメで、浅いところに作るということです。実装していく
基本的に最初の方で
src/index.jsReactDOM.render( <App />, document.querySelector('#root') );のようにして開発していきますが、ここの配下にモーダルを作ると構造的に深くなってしまうので、モーダルを作るときにはroot以外にidを指定して場所を作ってあげてそこに対して作っていきます。
public/index.html<div id="modal"></div>上記は例としてid="modal"として新たに要素を作っています。
src/components/Modal.jsimport React from 'react'; import ReactDOM from 'react-dom'; const Modal = props => { return ReactDOM.createPortal( <div className=""> <div className=""> This is a Modal!! </div> </div>, document.querySelector('#modal') ); }; export default Modal;上記ではReactDOM.createPortalメソッドを使っており第一引数に表示したい画面の内容、第二引数に場所を指定しています。
適当にcssつけてみてください。
あとは表示したいコンポーネントで使ってあげるという感じです。まとめ
簡単にモーダルの作り方を説明しました。
参考になれば幸いです。
- 投稿日:2019-02-11T17:33:58+09:00
webエンジニアがViroReactでAR/VRアプリ入門!
ViroReactとは?
ViroReact is a developer platform for rapidly building AR/VR applications using React Native. Use a single code base for your AR and VR apps.
https://viromedia.com/viroreact/
環境構築
まぁnpmとかyarnはみんな入ってるとして...
brewでwatchman, yarnでreact-native-cliとreact-viro-cliを入れる
$brew install watchman $yarn global add react-native-cli $yarn global add react-viro-cli実機テストできるアプリをダウンロードしておきましょう
https://itunes.apple.com/us/app/viro-media/id1163100576?mt=8
ViroReactでAPI_KEYをゲッチュしておきましょう
新規プロジェクト作成
とりあえずinit
$react-viro init ViroSample --verbose$cd ViroSample $yarn installApp.jsの25行目らへんに先程げっとしたAPI_KEYを書く
App.js/* TODO: Insert your API key below */ var sharedProps = { apiKey:"API_KEY_HERE", }アプリ起動
$yarn startアプリが動くURLが出てくるので、それをViroMediaのアプリにコピペします
http://xxxxxx.ngrok.ioさっきのURLをアプリ上に貼ると
無事にハローワールドできました!
- 投稿日:2019-02-11T16:37:59+09:00
React&ES6でカウンターアプリ作ってみた。
Reactとは、Facebookが中心に開発しているライブラリのことです。
FacebookやInstgram,Skypeなどで使用されています。
かなり複雑なUI(User Interface)でも効率的に開発できます。
*UI:ユーザーの目に触れる部分のこと。ホームページ上のデザインやフォントなどの外観ですね。本当は今回は、ES6を勉強しようと思ってたんですが、「ドットインストールReact入門」によると(またドットインストールかよ、という突っ込みは一切受け付けません。)、React入門の講座でES6もたくさん出てくるということだったので、Reactメインで勉強しました。
〇参考サイト
ドットインストールReact入門
https://dotinstall.com/lessons/basic_reactjs〇作るもの
商品の在庫数をカウントできる、カウンターアプリ
カウンターをクリックすれば、果物の個数が増えていきます。
2時間あればできるので、ぜひ!
それでは作っていきましょう。*本記事投稿者はプログラミング入門者です。間違っていることがありましたら、指摘してくださると大変嬉しい所存です。 https://www.miraidenshi-tech.jp/intern-content/program/
〇初期設定
まず初めに、Chromeの拡張機能である、React Developer Toolsを導入します。検索すれば出てきます。
また今回は、ローカルファイルで開発するので、「拡張機能を管理」で「ファイルへのアクセスを許可する」をONにしてください。
そうすれば、上の画像のように赤いカッコいいマークが出てきます。
〇コード
コードの説明はコメントでちょこちょこ書いています。
headのscriptの部分はReactの公式サイトの開発用スクリプトをコピペしています。<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>React Practice</title> <link rel="stylesheet" href="MyreactApp/css/styles.css"> <!-- Reactの本体 --> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <!-- Reactの結果をブラウザのDOMに反映させていくためのライブラリ --> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <!-- JSXやES6の文法を使うためのBabelというライブラリ --> <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script> </head> <body> <!-- Reactを使ったUIを表示する領域を作る --> <div id="root"></div> <script type="text/babel"> //即時関数で囲いたい→JSXの記法であるアロー関数式 (() => { // Props(propaties)がここに渡される function Counter(props){ return( <li style={{backgroundColor:props.counter.color}} onClick={() => props.countUp(props.counter)}> {props.counter.id}-{props.counter.count} </li> ); } // propsが渡ってくる function CounterList(props){ // 渡ってきたpropsのcountersをmapで処理 const counters=props.counters.map(counter => { return( <Counter //{}でJavaScriptの式を書ける←JSXの記法 counter= {counter} // ループ処理するために値を付ける key={counter.id} countUp={props.countUp} /> ); }); return( <ul> {counters} </ul> ); } // Counterの情報をこのstateで管理 stateを変更できるのはここだけ class App extends React.Component{ constructor(){ super(); this.state = { counters: [ {id: 'Melon', count: 0, color: 'limegreen'}, {id: 'Grape', count: 0, color: 'purple'}, {id: 'Apple', count: 0, color: 'red'}, {id: 'Orange', count: 0, color: 'orange'}, {id: 'Coconut', count: 0, color: 'white'}, {id: 'Lemon', count: 0, color: 'yellow'} ], total: 0 }; this.countUp=this.countUp.bind(this); } countUp(counter){ this.setState(prevState => { const counters=prevState.counters.map(counter => { return{id: counter.id, count: counter.count, color: counter.color}; }); const pos=counters.map(counter => { return counter.id; }).indexOf(counter.id); counters[pos].count++; return{ counters: counters, total: prevState.total + 1 }; }); } render(){ return( // HTMLで言うclassはclassNameで書く <div className="container"> <CounterList // ここにカウンターのデータが渡ってくる counters={this.state.counters} countUp={this.countUp} /> <div>TOTAL INVENTORY: {this.state.total}</div> </div> ); } } ReactDOM.render( <App/>, document.getElementById('root') ); })(); </script> </body> </html>
ここで仕組みを少し解説します。
Reactで重要なのが、
・Componet
・props
・state
です。
stateはUIの書き換えに必要となるComponentの変数を管理しています。
そして、propsはComponent間の変数の受け渡しをしています。ReactではComponent単位で細かく変数を管理することで、機能を拡張したり再利用したり、を簡単にしています。
〇つまずいたこと
ある時、エラーが出て、Developer Tools(Ctrl+Shift+iで表示)で確認したら、「〇行目が間違ってるよー」と出ました。その行のコードと30分以上にらめっこしていたが全く分からず。
原因は「,」を付けていないことでした。。。
- 投稿日:2019-02-11T15:11:27+09:00
ネイティブアプリの緩やかな死とSPAのメリデメ比較
はじめに
タイトルは半分釣りですが、半分本気でそう考えています。
現在SPAでWebアプリを作る仕事をしていますが、遠くない未来、ネイティブアプリの時代は終わって、SPAで作られたWebアプリが主流になると思っています。
今回はネイティブアプリに変わるかもしれないSPAについて、具体的にどんなものか、ネイティブアプリと比較した場合のメリデメ等も考えていきたいと思います。SPAとは何か?
SPAとは、SinglePageApplication(シングルページアプリケーション)の略称で、JavaScriptを使って、単一のページでコンテンツの切り替えを行う、Webアプリケーションを作る為の手法です。
私のポートフォリオサイトも、SPA(Nuxt.js)で作っています。
非同期通信を使って、ブラウザによるページ遷移を行わずに、コンテンツを切り替える事ができます。
裏側のHTMLはheadとCSSとJavaScriptしか存在せず、JavaScriptがDOMの切り替えを行なっています。Pjaxと何が違うの?
少し話が脱線してしまいますが、非同期通信が出来るjQueryライブラリに、Pjaxというものがあります。
Pjaxは、URLを変更すると非同期でコンテンツを入れ替えてくれるライブラリで、SPAとは違い遷移先にページが存在しています(HTMLがある)。
SPAの場合、実際には遷移先にページが存在しておらず、JavaScriptでDOMとURLを動的に切り替えて、リクエストがあった差分のみを描画しています。アプリ開発の現状
SPAは、非同期通信による滑らかな挙動、豊富なライブラリ、安全な認証等、ネイティブアプリと比較しても遜色ない環境が揃っていますが、まだ広く普及しているとは言い難いと思います。
裏側の管理画面や、単一ページをSPAで開発しているケースはありますが、アプリケーションそのものをSPAで開発しているプロダクトは、まだそう多くはないのではないでしょうか?
エンジニア的なコストはあまり詳しくはないですが、AWSやモジュール設計、セキュリティやパフォーマンスを意識した要件定義、およびコーディングが出来る人はそう多くありません。
デザイナー的にも、通常のWebアプリと同等の設計、つまりPC・タブレット・スマホ毎に設計が必要だったり、参考サイトやノウハウが少ない中での設計、ネイティブアプリと比べて足りないものを別のアプローチで補わないといけなかったりするので、要件定義や設計手法が手探りになる事が多いです。SPAのメリット
ネイティブアプリと比較して、運用コストが低い
運用コストが低いというより、ネイティブアプリの運用コストが高すぎるのかもしれません。
後述する様にクロスプラットフォームである為、デバイスによって言語が違うということはありません。全てJavaScriptで動作するので、Webブラウザがあればすぐ使う事ができます。
デザインも、iOS・Androidで別に用意するという必要性はなく、またブラウザで動くアプリケーションなので、PCで使用する事も可能です。ストアによる審査がない
Webアプリケーションなので、ネイティブアプリと違いストアによる審査が必要ありません。
プロダクトが完成したら、サーバーにプッシュして、デプロイするだけですぐに反映されます。リジェクトされる心配はありません。
また、ストアに支払う登録料も維持費も必要ありませんし、ストアによる高い決済手数料を気にする必要もありません。ネイティブアプリに近い挙動を実現できる
SPAはJavaScriptを使って非同期によるページ遷移を行うので、ネイティブアプリに近い挙動を実現できます。
ネイティブアプリと完全に同じ事は出来ませんが、非同期による滑らかな体験は、従来のWebアプリケーションには無かった強力な武器となります。
データバインディングが出来るので、ネイティブアプリの様に、URLを変えずにタップしたら要素だけ変える、という様な事も可能です。クロスプラットフォームである
SPAはブラウザで動くので、デバイスを選ばないクロスプラットフォームです。これもSPAの強力な武器となります。
基本的にネイティブアプリは、iOS / Androidで言語も開発チームも異なるので、チームは同じでも同時進行で開発する事はあまりないかと思います。
もちろん、ブラウザによる細かい挙動や仕様を考慮する必要性はありますが、iOS / Androidで同じプロダクトを別で作ることに比べたら、コストは微々たるものです。豊富な開発環境
今ではネイティブアプリで使える環境が、SPAを使えばWebアプリケーションでも使える様になってきています。
例えば、私のサイトでも認証で使われているFirebase Authenticationは、ネイティブアプリでもWebでも使う事が出来ます。SPAのデメリット
プッシュ通知が使えない
現状SPAの一番の課題になっているのは、このプッシュ通知が使えない事ではないでしょうか?
一応、WebPushという技術を使えば限定的に使う事はできますが、それでもiOS/Safariではサポートされておりませんので、必然的にiPhoneでは使えない事になります。
プッシュ通知が欲しい場合は、別のアプローチを試みる必要があります。デザイナー・エンジニア共に経験者が少ない
SPAはまだ現状のアプリケーションの主流になりきれていないので、デザイナー・エンジニア共に経験者が少ないのが現状です。
業務ページ、単一ページをSPAで作成される事はあっても、プロダクトそのものにSPAが使われているケースは、国内ではまだ多くありません(海外の場合、Youtube・Google・JIRA等の前例はあります)。高度なJavaScriptの技術が必要
正確にいうとデメリットではありませんが、SPAは今までサーバーサイドで行なっていた処理を、JavaScriptを使ってクライアントサイドで実行するので、必然的に高いJavaScriptの技術が必要になります。
SEO対策に注意が必要
現在はそうでもありませんが、SPAは裏側にHTMLが存在せず、JavaScriptを使って動的にDOMを切り替えるので、ほとんどがクローリングの対象外です。この問題を解決する為には、別途SEO対策をする必要があります。
Nuxt.jsであれば、modeを変更するだけで簡単にSEOで必要な要素を書き出してくれるので、現在ではあまり心配する必要はないかもしれません。初期表示に時間がかかる
初期ページで必要なものを全て読み込む性質上、初期表示に時間がかかる事が多いです。
アナリティクスの設定が独特
慣れの問題なのですが、SPAでGoogleアナリティクスを導入する場合、公式から出ているライブラリを導入して、別途設定をする必要がある為、ネットによくある普通のサイトにアナリティクスを設定するやり方は、通用しない事がほとんどです。
また、通信が発生しない都合上通常のやり方ではイベントトラッキングが出来ないので、トラッキングさせるには、別途特殊な設定が必要だったりもします。ジェスチャーが使えない
まだまだJavaScriptでは、ネイティブアプリの様な豊富なジェスチャーは使えないのが現状です。
ですが、ほとんどのジェスチャーはボタンアクションでサポート出来るので、あまり困る事もないですが。ネイティブアプリのメリット
SPAのメリデメと反するものが多いですが、整理する為に改めて書いてみます。
プッシュ通知が使える
SPAと比較して、一番のメリットはこれではないでしょうか。
ネイティブアプリはプッシュ通知によるアプローチが出来るので、アプリはインストールしたけど使っていない様な休眠ユーザーを掘り起こしたり、PRしたいものを効率的にアプローチする事が出来ます。ホーム画面に追加してもらえる
SPAとネイティブアプリで挙動が変わらないのであれば、インストールしてもらう事で生まれるメリットは、これではないでしょうか。
SPAでもホーム画面にアイコンを追加してもらう事は出来ますが、Androidはホーム画面にアイコンを追加する画面を出せるのでいいですが、iOSの場合はまだサポートされていませんので、ホーム画面に追加する場合、Safariから自分でやってもらうしか方法がありません。ノウハウが豊富
ストアにネイティブアプリが豊富にあるので、参考事例を探すのに困る事はありません。
ジェスチャーが使える
ボタンアクションで賄えますが、表現が豊富であればアプローチできる幅が広がるので、ジェスチャーを使えるのは魅力的だと思います。
PCのデザインを作らなくていい
ネイティブアプリの場合スマホで動くので、PCのデザインを作らなくて良いのは、工数が一つ減ることになります。
ネイティブアプリのデメリット
運用コストがとてもかかる
これがネイティブアプリの一番のデメリットだと思います。
SPAが登場する以前は致し方なかったですが、SPAが出てきた今、Webアプリケーションとネイティブアプリの差異は、プッシュ通知以外ほぼ無いと言っても良いかもしれません。
そうなるとSPAと比較して、プラットフォームが異なるiOS/Androidでアプリを開発するのは、運用コストがもの凄くかかってきます。
ストアに支払う維持費や決済手数料もとても高いです。インストールに時間がかかる
SPAの場合ネットに繋がないと使えませんが、裏を返せばそれは、インストールしなくてもページさえ開けば、すぐ使えるということです。
ネイティブアプリの場合インストールしないと使えないので、蓋を開けるまでどの様なアプリか分かりません。
よく分からないアプリを開く為に、わざわざインストールするでしょうか?
Webと違ってネイティブアプリの場合、広告を出してもその後「使ってもらう」為の工夫が必要なのです。ストアの審査がある
ストアによる審査があるので、ネイティブアプリの場合本番に反映されるのが遅いです。
バージョン管理が大変
あまり詳しくは無いですが、アプリの場合バージョンを上げていかないとリジェクトされて更新が出来なくなるので、定期的にバージョンを上げていかないといけません。
SPAならサーバーにプッシュすればいいだけですが、アプリはそうもいかないみたいです。まとめ
まとめますと、運用コストがめちゃくちゃかかるネイティブアプリより、今ならSPAで開発する方が遥かに効率的では?ということです。
SPAも開発コストがかかるので、ブログの様な直帰率が高いサービスにはオーバースペックかもしれませんが、滞在時間が長いアプリケーションにはとても適しています。
- 投稿日:2019-02-11T08:47:36+09:00
更にReact Hooksだけでライブラリ使わずにgoogle mapを利用する(応用編)
こちらはReact Hooksだけでライブラリ使わずにgoogle mapを利用する(基礎編)の続きになります。
Google MapのReact Hooksでの利用。前回マップを出すとこまでをとしてまとめた。
ここからは応用編としていろんな手段を書いて行きたい。
- 3. クリックしたらマーカーを増やすようにする
- 4. クリックしたら削除もするようにする
- 5. InfoWindowを出す
応用のため遠慮せずすべてのhooksを色々利用していく。
なるべく説明は書いているが不足を感じたら公式ドキュメントをご参照いただきたい
https://reactjs.org/docs/hooks-reference.htmlまた要所要所でリファクタを挟んでいる。
コードは前回の続きからとしてご覧いただきたい。3. クリックしたらマーカーを増やすようにする
クリックされたらマーカーを増やすようなことを試してみる。
追加するhooksは下記の2つになる
// hooks.js // markerをstate管理する export const useMarkerState = (initialMarkers) => { const [markers, setMarkers] = useState(initialMarkers) // マーカーの追加処理はsetMarkersを加工する形に const addMarker = ({ lat, lng }) => { setMarkers([...markers, { lat, lng }]) } return { markers, addMarker } } // Mapがクリックされたらイベントを飛ばすhooks export const useMapClickEvent = ({ onClickMap, googleMap, map }) => { useEffect(() => { if (!googleMap || !map) { return } const listener = googleMap.maps.event.addListener(map, "click", (e) => { onClickMap({ lat: e.latLng.lat(), lng: e.latLng.lng() }) }) // onClickMapが変更されたらつくったイベントをクリアする //(じゃないとクリックするたびにイベントが大量閣下さえる) return () => { googleMap.maps.event.removeListener(listener) } }, [googleMap, map, onClickMap]) }また、
useMapMarker
も書き換える。
markerが多重描画されないように、markerの実体を保存する。
stateで保持しようとすると遅延アップデートがされる都合でうまく保持がされないためexport const useDrawMapMarkers = ({ markers, googleMap, map }) => { // stateだと初回描画ほ保持がうまくいかないのでここではrefを利用する const markerObjectsRef = useRef({}) // markersが変わるたびに実行する useEffect(() => { // 初期化がまだなら何もしない if (!googleMap || !map) { return } const { Marker } = googleMap.maps markers.map((position, i) => { if (markerObjectsRef.current[i]) { // すでに描画済みだったら何もしない。 return } const markerObj = new Marker({ position, map, title: "marker!" }) markerObjectsRef.current[i] = markerObj }) }, [markers, googleMap, map]) }addMarkerのところは
useCallback
を利用してもいいだろうconst addMarker = useCallback(({ lat, lng }) => { setMarkers([...markers, { lat, lng }]) }, [markers]) // markersが変更したら関数自体を変える。そうしないとstateの状態とmapの実体がずれてくるそして利用する側がこんな具合になるだろう。
export const MapApp = () => { const googleMap = useGoogleMap(API_KEY) const mapContainerRef = useRef(null) const map = useMap({ googleMap, mapContainerRef, initialConfig }) // stateとして管理するマーカー const { markers, addMarker } = useMarkerState(initialMarkers) // 描画する useDrawMapMarkers({ markers, googleMap, map }) // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) return ( <div style={{ height: "100vh", width: "100%" }} ref={mapContainerRef} /> ) }3をちょっとリファクタ
ちょっとMapAppが膨れてきてしまったのでリファクタしてみる。
MapをマウントするコンテナのCSS部分をstyled-components化するのとマーカーのhooksを利用するだけのコンポーネントで分離するimport styled from "styled-components" // コンテナのCSS部分をstyled-componentsにする const MapContainer = styled.div` height: 100vh; width: 100%; ` // マーカーのhooksを利用する const MapMarkers = ({ googleMap, map }) => { // stateとして管理するマーカー const { markers, addMarker } = useMarkerState(initialMarkers) // 描画する useMapMarker({ markers, googleMap, map }) // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) // hooksのためだけのコンポーネントになるのでこのコンポーネント自体は何も返さない。 // nullを返すのが気持ち悪ければ`<script />`, `[]`, `""`を返すなどもアリ return null } export const MapApp = () => { const googleMap = useGoogleMap(API_KEY) const mapContainerRef = useRef(null) const map = useMap({ googleMap, mapContainerRef, initialConfig }) return ( <> <MapContainer ref={mapContainerRef} /> <MapMarkers googleMap={googleMap} map={map} /> </> ) }hooksを利用するだけのコンポーネントがはて良いものかというのは少し悩むところにも感じる...
hooksの部分を独自に切り出して下記のようにするのも良いだろう
// hooksを単純に呼び出してるhooks const useMapMarkerSetup = ({ googleMap, map }) => { const { markers, addMarker } = useMarkerState(initialMarkers) useMapMarker({ markers, googleMap, map }) useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) } const MapMarkers = ({ googleMap, map }) => { useMapMarkerSetup({ googleMap, map }) return null }また、例えばこんな風にMapが初期化されるまで待つようなコンポーネントを作る事もできるだろう
const WaitForMap = ({ googleMap, map, children }) => { if (!googleMap || !map) { return null } return children } export const MapApp = () => { const googleMap = useGoogleMap(API_KEY) const mapContainerRef = useRef(null) const map = useMap({ googleMap, mapContainerRef, initialConfig }) return ( <> <MapContainer ref={mapContainerRef} /> <WaitForMap googleMap={googleMap} map={map}> <MapMarkers googleMap={googleMap} map={map} /> </WaitForMap> </> ) }// hook.js export const useMapMarker = ({ markers, googleMap, map }) => { useEffect(() => { // このチェックが不要になる // if (!googleMap || !map) { // return // } const { Marker } = googleMap.maps // ...DEMO
https://gist.github.com/terrierscript/d8c9665f1a1761c48ee3739c0350ab76
4. クリックしたら削除もするようにする
更に応用。クリックされたら削除されることを考えてみる。ちょっとここからはだいぶ複雑度が増してくる。
ここでは下記2つの方法を示す
- A: markersを配列として描画する
- B: markersを一つずつのコンポーネントとして処理する
共通部分: Stateをreducer化する
ここまでマーカーは配列で処理してきたが、削除のことまで考えるとobjectで暑かったほうが都合が良くなるので
useReducer
を利用してreducer化する。import uuid from "uuid/v4" const markerReducer = (state, action) => { switch (action.type) { case "ADD": const id = uuid() // 追加するたびにuuidをidとして発行 return { ...state, [id]: action.payload } case "REMOVE": const { [action.payload]: removeItem, ...rest } = state return rest default: return state } } // 初期データもreducerを通してあげたいので、initializerを作成 const mapReducerInitializer = (initialMarkers) => { return initialMarkers.reduce((state, marker) => { return markerReducer(state, { type: "ADD", payload: marker }) }, {}) } // markerをstate管理する export const useMarkerState = (initialMarkers) => { const [markers, dispatch] = useReducer( markerReducer, initialMarkers, mapReducerInitializer ) // マーカーの追加・削除のaction関数 // ここも効率化のためにuseCallbackを使っているが必須ではない。 // const addMarker = (position) => dispatch({ type: "ADD", payload: position }) などでも十分だろう const addMarker = useCallback( (position) => dispatch({ type: "ADD", payload: position }), [dispatch] ) const removeMarker = useCallback( (removeUuid) => dispatch({ type: "REMOVE", payload: removeUuid }), [dispatch] ) // 外向けにobjectではなくarrayとして返すためのselector const getMarkers = useCallback( () => Object.entries(markers).map(([id, position]) => ({ id, position })), [markers] ) return { // markers // 元のobjectとしてのmarkerは隠蔽する addMarker, removeMarker, getMarkers } }4-A: markersを配列として描画する
先程までのuseDrawMapMarkersを拡張する形でまずは書いてみる。
こちらの方が手軽といえば手軽だろうexport const useDrawMapMarkers = ({ markers, googleMap, map, onClickMarker }) => { // マーカーの再描画を防ぐためrefsに保持 const markerObjectsRef = useRef({}) useEffect(() => { const { Marker } = googleMap.maps markers.map(({ id, position }) => { // すでに描画済みなmarkerだったら描画しない if (markerObjectsRef.current[id]) { return } const markerObj = new Marker({ position, map, title: "marker!" }) // markerがクリックされた時のイベントを追加する markerObj.addListener("click", (e) => { onClickMarker(id, markerObj, markerObjectsRef.current, e) }) markerObjectsRef.current[id] = markerObj }) }, [markers, googleMap, map]) }利用側はこんな感じになる
const useMapMarkerSetup = ({ googleMap, map }) => { // stateとして管理するマーカー const { addMarker, removeMarker, getMarkers } = useMarkerState(initialMarkers) const markers = getMarkers() // 描画する useDrawMapMarkers({ markers, googleMap, map, // 削除イベントを追加 onClickMarker: (id, markerObj, markerObjectsRef) => { removeMarker(id) markerObj.setMap(null) markerObjectsRef[id] = null } }) // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) } const MapMarkers = ({ googleMap, map }) => { useMapMarkerSetup({ googleMap, map }) return null }なかなか分厚い状態なのと、Markerのオブジェクトを利用側でいじる形になるのは少々気持ちが悪いかもしれない。
コード
https://gist.github.com/terrierscript/861df9328f80f077ac0b534569f3e8e1
4-B: markersを一つずつのコンポーネントとして処理する
ということでこれを改修して、「マーカー1つ1つにコンポーネントとフックを対応させる」という方向でやってみる。
export const useDrawMapMarker = ({ position, googleMap, map, onClickMarker }) => { const markerObjectsRef = useRef(null) useEffect(() => { const { Marker } = googleMap.maps // すでに描画済みなmarkerだったら描画しない if (markerObjectsRef.current) { return } const markerObj = new Marker({ position, map, title: "marker!" }) // markerがクリックされた時のイベントを追加する markerObj.addListener("click", (e) => { onClickMarker(e) }) markerObjectsRef.current = markerObj // effectの返却の関数として、コンポーネントが消え場合の処理を書けるので、ここでmarkerもmapから消すように仕掛ける。 return () => { if (markerObjectsRef.current === null) { return } markerObjectsRef.current.setMap(null) } }, [googleMap, map]) }利用側はこのようになる
// marker一つ一つを担当するComponent const Marker = ({ googleMap, map, position, onClickMarker }) => { useDrawMapMarker({ googleMap, map, position, onClickMarker }) return null } const MapMarkers = ({ map, googleMap }) => { const { addMarker, removeMarker, getMarkers } = useMarkerState(initialMarkers) const markers = getMarkers() // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) return ( <> {markers.map(({ id, position }) => ( <Marker key={id} // hooksがkeyに紐づく。これがないと適切なマーカーが消えなくなる position={position} onClickMarker={() => { removeMarker(id) }} map={map} googleMap={googleMap} /> ))} </> ) }こうなってくると「なんだかhooks以前のReactと手間変わらない気もする・・・」というのもあるのだが、結局はこの方がスマートになる印象だ。
コード
https://gist.github.com/terrierscript/f57ace64b776848d68be6b5bc736e2ed
リファクタ: onClickイベントを分離する・
先程までの
useDrawMapMarker
だとイベントが変更しても感知しないものになっていた。
もう少しこの点きれいに考えると下記のようになるだろう。
useDrawMapMarker
から帰ってくるMarkerオブジェクトを別途effectしてフックするような形になる。また、これまでhooksの内部でしか使っていなかったため
useRef
で良かったが、Markerが外部に出すものとなったのでuseState
で書き換えている。export const useDrawMapMarker = ({ position, googleMap, map }) => { const [markerObject, setMarkerObject] = useState(null) useEffect(() => { const { Marker } = googleMap.maps // すでに描画済みなmarkerだったら描画しない if (markerObject) { return } const markerObj = new Marker({ position, map, title: "marker!" }) setMarkerObject(markerObj) // コンポーネントが消えたらmarkerもmapから消すように仕掛ける。これはすっ return () => { if (markerObj === null) { return } markerObj.setMap(null) } }, [googleMap, map]) // markerObjectを更新対象にするとすぐ消えてしまうので、対象にしないようにする。この辺の勘所ちょっと慣れが必要そう return markerObject } export const useMarkerClickEvent = (marker, onClickMarker) => { // イベントが変更される事を考慮する useEffect(() => { if (!marker) { return } const listener = marker.addListener("click", (e) => { onClickMarker(e) }) return () => { listener.remove() } }, [marker, onClickMarker]) }const Marker = ({ googleMap, map, position, onClickMarker }) => { const marker = useDrawMapMarker({ googleMap, map, position }) // イベントの呼び出しはこっちで行う useMarkerClickEvent(marker, onClickMarker) return null }もう一つリファクタ: Context化する。
ここまでgoogleMapとmapをやたらと引き回してしまった。
そろそろこの辺をContext化する(正直もうちょっと手前でやっておけば良かった感じがある)export const MapContext = createContext({ googleMap: null, map: null })途中で作成したWaitForMapのタイミングが
Provider
をするのにちょうどよいだろうconst WaitForMap = ({ googleMap, map, children }) => { if (!googleMap || !map) { return null } const value = { googleMap, map } return <MapContext.Provider value={value}>{children}</MapContext.Provider> }例えば
useMapMarkerSetup
などは引数が不要になる。
利用側より親でProviderで囲む事を忘れないようにだけ注意が必要だconst useMapMarkerSetup = () => { const { googleMap, map } = useContext(MapContext) const { addMarker, removeMarker, getMarkers } = useMarkerState(initialMarkers) const markers = getMarkers() useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) return { markers, removeMarker } }5. InfoWindowを出す
最後にInfoWindowを出すところまでやってみる。
内容物としてDOMNodeかHTMLの文字列を渡さなければならずなかなか苦戦するものだった。準備:クリックで削除をダブルクリックで削除にする
クリックの処理をinfoWindowにさせたいので、削除処理はダブルクリックに移動させたい。
// Markerのイベントフックを柔軟にしてどのイベントにも対応できるようにする export const useMarkerEvent = ({ marker, eventHandler, eventName }) => { // イベントが変更される事を考慮する useEffect(() => { if (!marker) { return } const listener = marker.addListener(eventName, (e) => { eventHandler(e) }) return () => { listener.remove() } }, [marker, eventName, eventHandler]) }const Marker = ({ position, onDoubleClickMarker }) => { // 先程context化をしたのでgoogleMap/mapについては気にしなくしている const marker = useDrawMapMarker({position}) // イベントの呼び出しはこっちで行う useMarkerClickEvent({marker, eventName: "dblckick", eventHandler: onDoubleClickMarker) return null }InfoWindowを作る
ほとんどこれまでの応用になるので、あとはhooksとコンポーネントを作っていくだけになる
// googleMapのinfoWindowを生成して返す。 // contentNodeはDOM要素かstringなので、ここではDOMNodeを想定する export const useMapInfoWindow = (content) => { const [infoWindowState, setInfoWindow] = useState(null) const { googleMap } = useContext(MapContext) useEffect(() => { if (!content) { return } // infoWindowの再描画防止 if (infoWindowState) { return } const infoWindowObj = new googleMap.maps.InfoWindow({ content }) setInfoWindow(infoWindowObj) return () => { // 消す時はcloseする infoWindowObj.close() } }, [googleMap, content]) return infoWindowState }// 表には見せない要素。vueのv-cloakから名前を拝借した const Cloak = styled.div` display: none; ` // infoWindowを仕掛けるコンポーネント const MarkerInfoWindow = ({ marker, position }) => { const { map } = useContext(MapContext) const contentRef = useRef(null) // contentRefのDOMNodeを表示要素としたinfoWindowを作る const infoWindow = useMapInfoWindow(contentRef.current) useMarkerEvent({ marker, eventName: "click", // クリックしたら開く eventHandler: () => infoWindow.open(map, marker) }) return ( <Cloak> <div ref={contentRef}> <b>hello</b>, {position.lat}, {position.lng} </div> </Cloak> ) } const Marker = ({ position, onDoubleClick }) => { const marker = useDrawMapMarker({ position }) // ダブルクリックしたら消す useMarkerEvent({ marker, eventName: "dblclick", eventHandler: onDoubleClick }) // markerは先程nullにしていたが、InfoWindowを表示する役割が出来た return <MarkerInfoWindow marker={marker} position={position} /> }DEMO
ここだけCodesandboxのDEMOで。hooksファイルが肥大化してしまったので分割したバージョンにしている
https://codesandbox.io/s/wnk87oykzw感想など
- Hooksは単純なものをすごくライトに書けるようになるのはものすごく効果を感じた
- ちょっとreducerとaction書くだけであればすごく楽だし、reduxやvuexの頃の知識はほぼそのまま使える
- 地味に
selector
に相当する関数もhooksから返せるのはアリかもしれない- やはりそこそこ複雑度のあるコードにはそれなりの複雑度になってくると感じた(それでもだいぶ楽ではある)
- 曲線が変わった印象があり、複雑度が低い時が部分の難易度ぐぐっと下がって、複雑度が上がってくると徐々に変わらなくなってくる印象
- hooksはhooksでハマるタイミングがある
- (それでもやっぱりhooksのほうが楽なのは違いない)
- 誤解を恐れずにいうと「React先生がずーっと無限ループしててその途中でFunctional Componentが呼ばれていてその中でhooksの関数だけがその無限ループの外側で管理されてる」みたいな世界観を脳みそに宿しながら触っていると「あー、そういうことか」となる気がした(あっているかは微妙)
- 地味に4-Aで見せたような「少しReactらしくないコード」でも割となんとかなってしまうというのは結構利点に感じている。
useEffect
は今回のような外部のコード連携はとでも相性がよいし、返却の関数でclean upできるのも非常に良いと感じた。
- ただ更新対象とする値をうっかり間違えると躓くことがしばしばあった
useState
とかの値と組み合わせると無限レンダリングが走ったりするのをやってたりするので、そこらへんは注意点useState
はimmutableな分、mutableに扱える部分でuseRef
`に頼りたくなってしまうが、再レンダリングされず結構ハマるので注意したほうが良さそう
- 特に外部に出す変数は
useRef
はやめたほうが良さそう。ハマる。- 「hooksはトップレベルでしか使ってはいけません」というルールを迂回したくなるとヘンなコードを書いてしまいそうになるなーという感じがする。
- 今回もhooks使うだけで何もしないコンポーネントというのをやったりしてしまったが「ちょっと微妙かも・・・」という気持ちは出てくる
- このへんは今後ちょこちょこプラクティスが出てくる予感がするかもしれない
- 投稿日:2019-02-11T00:16:35+09:00
React で型の named import は辞めよう
React コーディング全てではなく、主に SyntheticEvent 型を継承している DOM 周りの型に限っては、named import を避けるべきです。一見エラーにならない様に感じる次のコードですが、onClick にバインディングする行でエラーが発生します。
import React from 'react' import { MouseEvent } from 'react' type Props = { handleClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void } const Component: React.FC<Props> = props => ( <div> <button onClick={props.handleClick}></button> // Error </div> )型名称の衝突
React.MouseEvent<T = Element, E = NativeMouseEvent>
と定義元にある様に、Generics 2番目は、Native の Event型を期待しています。例えばブラウザ向けのプロジェクトの場合、それはlib.dom.d.ts
で global定義されているMouseEvent
を指します。そのため、
@types/react
から named import したMouseEvent
と衝突してしまい、エラーになってしまっていた、という訳です。SyntheticEvent 型を継承している@types/react
提供型と、lib.dom.d.ts
で global定義 されている型はこんなに被っています。x ClipboardEvent x CompositionEvent x DragEvent x PointerEvent x FocusEvent FormEvent InvalidEvent ChangeEvent x KeyboardEvent x MouseEvent x TouchEvent x UIEvent x WheelEvent x AnimationEvent x TransitionEventそのコンテキストで多様し冗長だから、という理由で型を named import すると、無駄に時間を溶かしてしまうかもしれないので注意しましょう。非常にややこしいですね。