React Hooks とdeck.gl v.7を使ってweb地図を作ってみた。
以前紹介したUber社製のWeb地図フレームワーク「Deck.gl」のver.7が公開されました。紹介した頃はver.5だったのでだいぶ間があき、公式のチュートリアルサイトVis Academyの内容も改編されていたので改めて一からチュートリアルをやり直してみることにしました。
Vis Academy – Building a Geospatial App
ただ、改編されたとはいえ一度やったことのあるチュートリアルをただなぞるのもなんなので、公式ではclassベースで書かれているコードを勉強がてらReact Hooksの機能を使ってfanctionベースに書き換えながら進めてみています。
例えば第1章のコードなら、以下のように変更しています。
元のコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import React, { Component } from 'react'; import MapGL from 'react-map-gl'; import {MapStylePicker} from './controls'; export default class App extends Component { state = { style: 'mapbox://styles/mapbox/light-v9', viewport: { width: window.innerWidth, height: window.innerHeight, longitude: -74, latitude: 40.7, zoom: 11, maxZoom: 16 } } componentDidMount() { window.addEventListener('resize', this._resize); this._resize(); } componentWillUnmount() { window.removeEventListener('resize', this._resize); } onStyleChange = (style) => { this.setState({style}); } _onViewportChange = (viewport) => { this.setState({ viewport: { ...this.state.viewport, ...viewport } }); } _resize = () => { this._onViewportChange({ width: window.innerWidth, height: window.innerHeight }); } render() { return ( <div> <MapStylePicker onStyleChange={this.onStyleChange} currentStyle={this.state.style}/> <MapGL {...this.state.viewport} mapStyle={this.state.style} onViewportChange={viewport => this._onViewportChange(viewport)} > </MapGL> </div> ); } |
React Hooks 使ってみたコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import React, { useEffect, useState } from 'react'; import MapGL from 'react-map-gl'; import { MapStylePicker } from './controls'; const MAPBOX_TOKEN = ''; export default () => { const [ mapboxStyle, setMapStyle ] = useState('mapbox://styles/mapbox/light-v9'); const [ viewport, setViewport ] = useState({ width: window.innerWidth, height: window.innerHeight, longitude: -74, latitude: 40.7, zoom: 11, maxZoom: 16 }); //resize useEffect(() => { const handleResize = () => { setViewport((v) => { return { ...v, width: window.innerWidth, height: window.innerHeight }; }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return ( <div> <MapStylePicker currentStyle={mapboxStyle} onStyleChange={setMapStyle} /> <MapGL {...viewport} mapStyle={mapboxStyle} mapboxApiAccessToken={MAPBOX_TOKEN} onViewportChange={(v) => setViewport(v)} // <-second callback argument warningが出るのでWrap /> </div> ); |
とはいっても、ReactについてはDeck.glを使いたいがためにちょこっと勉強した程度で、Hookもいまいちよくわからないまま雰囲気でつかっているので、せいぜいuseStateぐらいしか使っていません。
それでも良ければ、Githubに章ごとのコードを置いておいたのでご覧ください。
全コード
ちなみにHexagon Layerでバグがあります。これ公式のサンプルでも同じように動作しているので、ちょっとまだ原因がわかってません。
あと、control.jsの一部で Stateless functionに変更できてないコンポーネントが残ってます。
Deck.gl ver.7について
個人的に気になった点についてだけ。
ベクトルタイルに対応
最後に触ったバージョンが5.2だったので実装されたのがいつなのかはわかりませんが、待望のtileLayerが実装されています。
サンプルではmapboxのベクトルタイルを読みこんでいて、コードを見る限りでは自分で作成したベクトルタイルを読み込むのも簡単そうです。
ただ、描画部分ではまだバグが多いようで表示欠けがおおく、また動作も結構重いです。
Mapboxへの依存度が減少
以前はDeck.gl OverLay コンポーネントの中に内包されていたreact-map-glコンポーネントを開発者が明示的に利用する形へと変更されたようです。
TileLayerの実装と相まってMapbox以外のベースマップを利用しやすくなりました。
これは、Google Maps PlatformによるDeck.glサポートが決定したことも影響しているのかもしれません。
React component vs Pure JavaScript library
Deck.glはver.52より、Reactを用いなくても通常のJavaScriptライブラリとして利用できるようになりました。公式サイトのドキュメントでは今のところ、両方での使い方が併記されていますが、最新の機能に関してはPure JavaScript libraryとして使った場合のサンプルコードしか記載されていないものがあります。
例えばGoogle Maps PlatformをDeck.glで利用するサンプルコードはPure JavaScript libraryでのコードしか公開されていません。
(というより、今のところReactコンポーネントとしてDeck.glコンポーネントと結合できるようになってないっぽい)
今後どちらの使い方がメインストリームになっていくのかわかりませんが、個人的にはReactを勉強するための良い動機になっていたので、React離れが進むことになるとちょっと残念かも。
React Hooksについて
useState使うだけでもコードがだいぶスッキリと整理されます。
例えば、”Building a Geospatial App”の最終章のレンダリング部分のコードを比較するだけでも、だいぶ読みやすくなったのではないかと。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
render() { const { viewState, controller = true } = this.props; const data = this.state.points; if (!data.length) { return null; } const { hover, settings } = this.state; return ( <div> {hover.hoveredObject && ( <div style={{ ...tooltipStyle, transform: `translate(${hover.x}px, ${hover.y}px)` }} > <div>{hover.label}</div> </div> )} <MapStylePicker onStyleChange={this.onStyleChange} currentStyle={this.state.style} /> <LayerControls settings={this.state.settings} propTypes={HEXAGON_CONTROLS} onChange={settings => this._updateLayerSettings(settings)} /> <DeckGL {...this.state.settings} onWebGLInitialized={this._onWebGLInitialize} layers={renderLayers({ data: this.state.points, hour: this.state.highlightedHour || this.state.selectedHour, onHover: hover => this._onHover(hover), settings: this.state.settings })} initialViewState={INITIAL_VIEW_STATE} viewState={viewState} controller={controller} > <StaticMap mapStyle={this.state.style} /> </DeckGL> <Charts {...this.state} highlight={hour => this._onHighlight(hour)} select={hour => this._onSelect(hour)} /> </div> ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
return ( <div> {hover.hoveredObject && ( <div style={{ ...tooltipStyle, transform: `translate(${hover.x}px, ${hover.y}px)` }} > <div>{hover.label}</div> </div> )} <MapStylePicker currentStyle={mapboxStyle} onStyleChange={setMapStyle} /> <LayerControls settings={layerSettings} propTypes={HEXAGON_CONTROLS} onChange={setLayerSetting} /> <MapGL {...viewport} mapStyle={mapboxStyle} mapboxApiAccessToken={MAPBOX_TOKEN} onViewportChange={(v) => setViewport(v)} > <DeckGL layers={renderLayers({ data: data.points, hour: data.selectedHour, settings: layerSettings, onHover: onHover })} viewState={viewport} onViewportChange={setViewport} initialViewState={INITIAL_VIEW_STATE} /> <Charts {...data} highlight={onHighlight} select={onSelect} /> </MapGL> </div> ); |
this消えるだけでも、だいぶスッキリした感じに。
個人的にJSでクラスベースのコード書くのあまり好きではなかったので、React Hooks使って行きたいなと思います。