ReactでGoogleMap APIを使いたい場合,google-map-reactやreact-google-mapsといったライブラリを使うのが一般的ですが,TypeScriptでReactアプリを作成している場合はどちらも型定義ファイルがないので,自分で型定義ファイルを書くかGoogleMapのコンポーネントを自作する必要があります.
自分の場合,使いたいのはGoogleMapとそのMarker程度だったので自作してみたのですが,すこし詰まった所があったのでメモをしておきます.
コードとポイント
とりあえずコードを読んで下さい.
/// <reference path="../../../node_modules/@types/googlemaps/index.d.ts"/>
import * as React from "react";
import * as ReactDOM from "react-dom";
interface GoogleMapProps {
}
interface GoogleMapState {
map: google.maps.Map;
}
export default class GoogleMap extends React.Component<GoogleMapProps, GoogleMapState> {
static childContextTypes = {
map: React.PropTypes.object
};
getChildContext() {
return { map: this.state.map };
}
state = {
map: null
};
constructor() {
super();
}
componentDidMount() {
const map = new google.maps.Map(
ReactDOM.findDOMNode(this.refs["top"]),
{
center: new google.maps.LatLng(0,0),
zoom: 18,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
);
this.setState({ "map": map });
}
render() {
if (this.state.map) {
return (
<div>
<div ref="top" style={{ height: 500 }}>
{this.props.children}
</div>
</div>
);
} else {
return (
<div>
<div ref="top" style={{ height: 500 }}>
</div>
</div>
);
}
}
}
/// <reference path="path/to/node_modules/@types/googlemaps/index.d.ts"/>
import * as React from "react";
interface MarkerContext {
map: google.maps.Map;
}
interface MarkerProps {
position: google.maps.LatLng;
}
interface MarkerState {
}
export default class Marker extends React.Component<MarkerProps, MarkerState> {
context: MarkerContext;
static contextTypes = {
map: React.PropTypes.object
};
constructor() {
super();
}
createMarker() {
const marker = new google.maps.Marker(
{
position: this.props.position,
title: "test",
label: "A"
}
);
marker.setMap(this.context.map);
}
componentDidMount() {
this.createMarker();
}
render() {
return (
<div>
</div>
);
}
}
このようにコンポーネントを定義しておくと,次のようにマップのViewが書けます.
import * as React from "react";
import GoogleMap from "./GoogleMap";
import Marker from "./Marker";
export default class MapPage extends React.Component<{}, {}> {
render() {
return (
<GoogleMap>
<Marker position={new google.maps.LatLng(0,0)} />
</GoogleMap>
)
}
}
ポイント
GoogleMapコンポーネントとMarkerコンポーネントはその本来の関係からも親コンポーネント,子コンポーネントとして下のように書きたいと思うのではないでしょうか.
<GoogleMap>
<Marker position={new google.maps.LatLng(0,0)} />
</GoogleMap>
ただ,これには問題があります.それはMapオブジェクトの共有がPropsを介して出来ないということです.Google Map API のMarker Classリファレンスを見ていただければ理解していただけるように,MarkerはsetMap
関数かコンストラクタ引数のオプションとしてmapをセットしてあげないと表示することは出来ません.ここで,Reactのcontextという機能を利用します.
React Contextについて
公式リファレンスのContext - Reactに
It is an experimental API and it is likely to break in future releases of React.
と書いてあるように,これは推奨されていないAPIですが,今回の目的を実現するためにはこれが不可欠となるので利用しました.
日本語の記事だと「React の Context を使って Flux を実装する - Qiita」などがわかりやすいかと思います.こちらの記事から説明を引用させていただくと,Contextとは
ある親の要素以下では、子供はある特定の親(おそらくは根)に依存した特定の this.context の状態を持てる機能
です.つまり,今回のように「親であるGoogleMapコンポーネントが持つ特定の状態(google.maps.Map オブジェクト)」を「子であるMarkerコンポーネントが状態として持ち利用したい」場合には適していると考えられます.実際に,react-google-mapsはcontextを利用してmapオブジェクトを子要素に渡しています.(参考)
実際に実装する場合,まず親となるコンポーネントでchildContextTypes
とgetChildContext()
を実装します.
static childContextTypes = {
map: React.PropTypes.object
};
getChildContext() {
return { map: this.state.map };
}
ここで,getChildContextにてGoogleMapコンポーネントの中にあるgoogle.maps.Map Objectを返すようにします.
次に,MarkerコンポーネントでcontextTypesを実装します.
static contextTypes = {
map: React.PropTypes.object
};
こうすると,Markerの中のthis.context.map
で親であるGoogleMapコンポーネントの中にあるmap Objectにアクセスができるようになりました.
ちなみにGoogleMapコンポーネントの中で
render() {
if (this.state.map) {
return (
<div>
<div ref="top" style={{ height: 500 }}>
{this.props.children}
</div>
</div>
);
} else {
return (
<div>
<div ref="top" style={{ height: 500 }}>
</div>
</div>
);
}
}
とstate.mapの状態によってrender関数内で分岐しているのは,google.maps.Marker classでsetMap
にnull
を渡してしまうと,リファレンスにある通り,
Renders the marker on the specified map or panorama. If map is set to null, the marker will be removed.
そのMarkerObjectが削除されてしまうからです.
おまけ - Contextを使わないでMarkerを実装
あまり非推奨なAPIを使いたくないという方のために他の実装も載せておきます.こちらのほうがシンプルかもしれません.
/// <reference path="path/to/node_modules/@types/googlemaps/index.d.ts"/>
import * as React from "react";
import * as ReactDOM from "react-dom";
import Marker from "./Marker";
interface GoogleMapProps {
markerPositions: google.maps.LatLng[];
}
interface GoogleMapState {
map: google.maps.Map;
}
export default class GoogleMap extends React.Component<GoogleMapProps, GoogleMapState> {
state = {
map: null
};
constructor() {
super();
}
componentDidMount() {
const map = new google.maps.Map(
ReactDOM.findDOMNode(this.refs["top"]),
{
center: new google.maps.LatLng(0,0),
zoom: 18,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
);
this.setState({ "map": map });
}
render() {
if (this.state.map) {
return (
<div>
<div ref="top" style={{ height: 500 }}>
{this.props.markerPositions.map((position) => {
<Marker positon={position} map={this.state.map}/>
})}
</div>
</div>
);
} else {
return (
<div>
<div ref="top" style={{ height: 500 }}>
</div>
</div>
);
}
}
}
/// <reference path="path/to/node_modules/@types/googlemaps/index.d.ts"/>
import * as React from "react";
interface MarkerProps {
map: google.maps.Map;
position: google.maps.LatLng;
}
interface MarkerState {
}
export default class Marker extends React.Component<MarkerProps, MarkerState> {
context: MarkerContext;
constructor() {
super();
}
createMarker() {
const marker = new google.maps.Marker(
{
position: this.props.position,
title: "test",
label: "A"
}
);
marker.setMap(this.props.map);
}
componentDidMount() {
this.createMarker();
}
render() {
return (
<div>
</div>
);
}
}
import * as React from "react";
import GoogleMap from "./GoogleMap";
import Marker from "./Marker";
export default class MapPage extends React.Component<{}, {}> {
positons = [
new google.maps.LatLng(0,0),
new google.maps.LatLng(1,1)
];
render() {
return (
<GoogleMap markerPositions={this.positions}/>
)
}
}
Contextを使わない場合とくらべてGoogleMapコンポーネントにMarkerPositionを指定する必要がありますが,実際同じように使うことは可能です(もし他にもMarkerを作成する上で指定したい物がある場合はPropsからmapを除いたinterfaceを定義して,それを使うといいかもしれません).
最後に
Reactを触り始めたのが最近であるため,これが一体正しい実装といえるのかあまり自信がありません.何か間違いなどがありましたら教えていただけるとありがたいです.