20190211のReactに関する記事は8件です。

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.js
import 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.js
if (process.env.NODE_ENV !== "development") {
    console.log = () => {};
}

npm run build実行時にはprocess.env.NODE_ENVにはproductionという文字列が入ってきますが、それがもし変更された際への考慮として逆のdevelopmentじゃない場合を条件にしています。
これなら変更あった際、開発段階でログ出力されなくなることから、変更にすぐ気が付くので(;^ω^)
一時的に出力されることに抵抗がないなら、if (process.env.NODE_ENV === "production")が直感的でいいかもしれません。

お手軽といったのには訳がある・・・

もう終了と思ったかもしれませんが注意点があります。
なぜなら、クライアント環境でこれを簡単に戻せる方法があるからです。
ブラウザのデベロッパーツールのコンソール開いて以下を打ち込みます。

console
console.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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.jsneutrino等の選択肢もあるが、こだわりも不満も特になかったので今回も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を使用。開発中とは言え、特にバグに遭遇することもなく安定していた。

機能

リスト

image.png

  • 一般的な検索・ソート・ページングができる
  • サムネイルは静止画の場合は最初のページを使用。動画の場合は再生時間の中央のイメージを使用。ただしブックマーク(後述)が登録されている場合はその最初の位置を使用。

ビューワ

静止画ビューワ

image.png

  • 画像からは分かりにくいけど、紙の本と同じように2ページ分を左右に並べて表示。(設定で1ページにもできる)
  • 普通の動画プレイヤーと同じ操作感で , でページ送りができる。通常は2ページ単位で移動するが、 shift+ , shift+ で1ページ単位になる。(見開きページ用の調整)
  • b でブックマークを設定できる。(スライダー上の白丸がブックマーク位置)同じ位置でまた bを押すとブックマークを解除。
  • , で現在ページを起点に前後のブックマークへ移動。

動画ビューワ

image.png

  • 機能的には静止画ビューワとほぼ同じ。
  • , での移動秒数は設定可能。(初期値は10秒)

メタデータの編集

image.png

  • リストまたはビューワからタイトルや作者・タグ等のメタデータを編集できる
  • タイトルを変更すると、実際のファイル名・ディレクトリ名も変更される。
  • ちなみに動画・静止画の最後まで閲覧すると視聴回数を自動インクリメントするため、編集項目に視聴回数(Viewed Count)は不要なはずだけど、開発時にインクリメントしすぎるバグがあり、回数を補正するために編集可能にした。今は当該バグはないけど、その名残。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React] モーダルの作り方

概要

今回はモーダルの作り方についてみていきます。
モーダルは項目を削除する時とかに出てくる画面のことで、「本当に削除しますか?」に対して「はい」、「いいえ」みたいに選択できるものです。

ポータル

モーダルは画面いっぱいを占領するので他の要素よりもトップに来なければいけません。
そこでポータルについて意識しないといけません。
簡単にいうと構造的に深くに作ってはダメで、浅いところに作るということです。

実装していく

基本的に最初の方で

src/index.js
ReactDOM.render(
  <App />,
  document.querySelector('#root')
);

のようにして開発していきますが、ここの配下にモーダルを作ると構造的に深くなってしまうので、モーダルを作るときにはroot以外にidを指定して場所を作ってあげてそこに対して作っていきます。

public/index.html
<div id="modal"></div>

上記は例としてid="modal"として新たに要素を作っています。

src/components/Modal.js
import 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つけてみてください。
あとは表示したいコンポーネントで使ってあげるという感じです。

まとめ

簡単にモーダルの作り方を説明しました。
参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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をゲッチュしておきましょう

https://viromedia.com/signup

新規プロジェクト作成

とりあえずinit

$react-viro init ViroSample --verbose
$cd ViroSample
$yarn install

App.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をアプリ上に貼ると

Image from iOS (1).png

無事にハローワールドできました!

Image from iOS (2).png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React&ES6でカウンターアプリ作ってみた。

Reactとは、Facebookが中心に開発しているライブラリのことです。
FacebookやInstgram,Skypeなどで使用されています。
かなり複雑なUI(User Interface)でも効率的に開発できます。
*UI:ユーザーの目に触れる部分のこと。ホームページ上のデザインやフォントなどの外観ですね。

本当は今回は、ES6を勉強しようと思ってたんですが、「ドットインストールReact入門」によると(またドットインストールかよ、という突っ込みは一切受け付けません。)、React入門の講座でES6もたくさん出てくるということだったので、Reactメインで勉強しました。

〇参考サイト
ドットインストールReact入門
https://dotinstall.com/lessons/basic_reactjs

〇作るもの
商品の在庫数をカウントできる、カウンターアプリ
果物カウンターQiita.jpg
カウンターをクリックすれば、果物の個数が増えていきます。
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分以上にらめっこしていたが全く分からず。
原因は「,」を付けていないことでした。。。
「,」.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ネイティブアプリの緩やかな死と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も開発コストがかかるので、ブログの様な直帰率が高いサービスにはオーバースペックかもしれませんが、滞在時間が長いアプリケーションにはとても適しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

更に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使うだけで何もしないコンポーネントというのをやったりしてしまったが「ちょっと微妙かも・・・」という気持ちは出てくる
    • このへんは今後ちょこちょこプラクティスが出てくる予感がするかもしれない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 すると、無駄に時間を溶かしてしまうかもしれないので注意しましょう。非常にややこしいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む