20190523のReactに関する記事は5件です。

認証情報をreduxで管理してreact-routerのRedirectを学ぶ

目標

題名の通り、reduxで管理した情報に基づきRedirectを行います。
具体的に実装するのは、ログインした人じゃないと見れないページを作るというものです。

実装していく

まずはトップのルーティングの部分

App.js
import PrivateRoute from './PrivateRoute';

const App = () => {
  return (
    <Switch>
      <PrivateRoute path="/" exact component={TopPage} />
      <Route path="/login" component={Login} />
      <Route component={NotFoundPage} />
    </Switch>
  );
};

ここでPrivateRouteというものを自分で作っています。
PrivateRouteをみていきましょう。

PrivateRoute.js
import React from 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';

const PrivateRoute = ({user, component: Component, ...rest}) => {
  return (
    <Route
      {...rest}
      render={props => 
        user ? (
          <Component {...props} />
        ) : (
          <Redirect to='/login' />
        )
      }
    />
  );
}

const mapStateToProps = state => {
  return {
    user: state.auth.user
  }
}

export default connect(mapStateToProps)(PrivateRoute);

ここでreduxで管理しているuserの真偽により、偽であれば/loginRedirectします。

reducers/authReducer.js
const initialState = {
  user: false
}

export default (state = initialState, action) => {
  switch(action.type) {
    case 'LOG_IN':
      return { ...state, user: true };
    case 'LOG_OUT':
      return { ...state, user: false };
    default:
      return state;
  }
}

お試しに実装したのでreducerでは大したことはしていません。
あとでfirebaseでのGoogleログインにしようと思っているのでその時は少し考えなければいけないかも。

最後にログインのコンポーネント

components/Login/index.js
import React from 'react';

import { connect } from 'react-redux';
import { login } from '../../actions';

class Login extends React.Component {

  reduxLogin = () => {
    const { login, history } = this.props;

    login();
    history.push('/');
  }

  render() {
    return (
        <div>
          <button onClick={this.reduxLogin}>login redux</button>
        </div>
    );
  }
}

export default connect(null, { login })(Login);

ログインのボタンを用意してログインするとトップページに飛ぶようにしています。
ログインしないでトップページに飛ぼうとしても/loginRedirectされます。

まとめ

今回はreact-routerRedirectについてみました。
参考になれば幸いです。

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

【Heroku】ルートに無いディレクトリ上で、デプロイ時にビルドを実行する

問題

Herokuはデプロイ時にルートにcomposer.jsonやpackage.jsonがあった場合、
jsonファイルを元に依存パッケージのインストールやアップデート、ビルド等行ってくれますが、ルートから外れたものは行ってくれません。

例えばこんな構成だと、package.json は無視されますしビルドは行われません。

ディレクトリ構成
.
├── react
│   ├── package.json ← 対象外
・・・
└── composer.json ← 対象

こういったルートから外れたjsonを対象にしたい場合、下記のように記述すればOK。

解決策

ルートにpackage.jsonのシンボリックリンクを作成

ディレクトリ構成
.
├── react
│   ├── package.json
・・・
├── package.json(react/package.jsonのシンボリックリンク)
└── composer.json

package.jsonに追記。

react/package.json
// package.jsonのscriptsに下記スクリプトを追加

"scripts": {
    ・・・
    "heroku-postbuild": "cd ./ビルド対象のディレクトリ名 && npm run build"
},

scripts内でshell打てるんだから、対象ディレクトリに移動してビルドすればいいじゃない、
と答え見た時は愕然としました。
こういうのは頭柔らかくしないといけませんね。

参考:
https://github.com/heroku/heroku-buildpack-nodejs/issues/323

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

ReactでGoogle mapをお届けする

はじめに

ReactでMapを使用し以下要件を満たす必要があった

・ マップスタイルの変更
・ ピンスタイルの変更
・ ピン間に線を描画する
・ ピンをクリックすると吹き出しが表示される

要件を満たすためにReactで使用可能なMapをレンダリングできるライブラリをざっと調査した

ライブラリ

ライブラリ ピン変更 バルーン機能 ルーティング 備考
google-maps-react Markerコンポーネントを提供している infoWindowで変更可能 polylineで設定可能 weekly download 24,085
google-map-react 子コンポ-ネント内に画像やスタイルを適用したコンポーネントで対応 子コンポーネントで吹き出しコンポーネントを配置しonChildClickで表示する onGoogleApiLoaded時に描画可能 weekly download 69,369
react-google-maps componentのcoordinates内で変更可能 infoWindowをカスタム onLoaded時に描画可能 ・weekly download 2,414
UI componentを豊富に提供
2gis-maps-react 細かいアイコンの設定が可能 popuupで設定可能 polylineで線を引ける ・no longer supported
・weekly download 15
react-map-gl markerで設定可能 popuupで設定可能 overlay機能を仕様しcanvasに線を引ける ・mapboxを使用可能
・weekly download 34,397

今回は単純にコミュニティがアクティブかどうか、公式サンプルが豊富かという視点でgoogle-map-reactを使用することにした

また、マップスタイルの変更としてsnazzymapsが豊富なサンプルを提供しているので今回使用することにした

実装

google-map-reactでピンを一個だけマップ上に表示したい場合はとても簡単に実装できる

import * as React from 'react'
import styled from 'styled-components'
import logo from './logo.svg'
import GoogleMapReact from 'google-map-react'

const App = () => (
  <Wrapper>
    <Header>
      <Logo src={logo} />
      <H1>Welcome to React</H1>
    </Header>
    <GoogleMapWrapper>
    <GoogleMapReact
      bootstrapURLKeys={{
        key: {'ここにgoogle map apiのkeyをドウゾ'}
      }}
      defaultCenter={{
        lat: 43.0582954,
        lng: 141.3466919
      }}
      defaultZoom={15}
    >
      <Pin
        lat={43.0582954}
        lng={141.3466919}
      >
      おおおんどおり
      </Pin>
    </GoogleMapReact>
    </GoogleMapWrapper>
  </Wrapper>
)

const Wrapper = styled.div`
  text-align: center;
`
const Header = styled.header`
  background-color: ${Color.Primary};
  height: 150px;
  padding: 20px;
  color: white;
`
const Logo = styled.img`
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
  @keyframes App-logo-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
`
const H1 = styled.h1`
  font-size: 1.5em;
`
const Pin = styled.div<{
  lat: number,
  lng: number
}>`
`

const GoogleMapWrapper = styled.div`
  height: 100vh;
  width: 100%;
`

export default App

これを適用すると大通公園に"おおおんどおり"と表示されてるはずだ
サンプルを参考にするだけでとても簡単にピンを配置できる

マップスタイル変更

Googleマップのデザインを以下のように変更したいケースがあると思う


実はsnazzymapというサイトを使えば簡単に実装できる

import * as React from 'react'
import styled from 'styled-components'
import { Color } from './brand'
import logo from './logo.svg'
import GoogleMapReact, { MapOptions, Maps } from 'google-map-react'

const App = () => {
  const createMapOptions = (maps: Maps): MapOptions => {
    return {
      mapTypeControlOptions: {
        position: maps.ControlPosition.TOP_RIGHT,
      },
      mapTypeControl: false,
      zoomControl: false,
      scaleControl: false,
      streetViewControl: false,
      fullscreenControl: false,
      styles: [
        {
          featureType: 'water',
          elementType: 'geometry',
          stylers: [
            {
              color: '#e9e9e9',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'landscape',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 29,
            },
            {
              weight: 0.2,
            },
          ],
        },
        {
          featureType: 'road.arterial',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 18,
            },
          ],
        },
        {
          featureType: 'road.local',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          featureType: 'poi',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          featureType: 'poi.park',
          elementType: 'geometry',
          stylers: [
            {
              color: '#dedede',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          elementType: 'labels.text.stroke',
          stylers: [
            {
              visibility: 'on',
            },
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          elementType: 'labels.text.fill',
          stylers: [
            {
              saturation: 36,
            },
            {
              color: '#333333',
            },
            {
              lightness: 40,
            },
          ],
        },
        {
          elementType: 'labels.icon',
          stylers: [
            {
              visibility: 'off',
            },
          ],
        },
        {
          featureType: 'transit',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f2f2f2',
            },
            {
              lightness: 19,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 17,
            },
            {
              weight: 1.2,
            },
          ],
        },
      ],
    }
  }
  return (
    <Wrapper>
      <Header>
        <Logo src={logo} />
        <H1>Welcome to React</H1>
      </Header>
      <GoogleMapWrapper>
      <GoogleMapReact
        bootstrapURLKeys={{
          key: ''
        }}
        defaultCenter={{
          lat: 43.0582954,
          lng: 141.3466919
        }}
        defaultZoom={15}
        options={createMapOptions}
      >
        <Pin
          lat={43.0582954}
          lng={141.3466919}
        >
        おおおんどおり
        </Pin>
      </GoogleMapReact>
      </GoogleMapWrapper>
    </Wrapper>
  )
}

const Wrapper = styled.div`
  text-align: center;
`
const Header = styled.header`
  background-color: ${Color.Primary};
  height: 150px;
  padding: 20px;
  color: white;
`
const Logo = styled.img`
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
  @keyframes App-logo-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
`
const H1 = styled.h1`
  font-size: 1.5em;
`
const Pin = styled.div<{
  lat: number,
  lng: number
}>`
`

const GoogleMapWrapper = styled.div`
  height: 100vh;
  width: 100%;
`

export default App

createMapOptionsのfullscreenControl optionの下にstyles optionがある
このstyles optionにsnazzymapで取得したソースコードをコピペするだけで実装できる

ちなみに今回はSubtle Grayscaleを適用した

ピンスタイルの変更

前述通りピンを1個だけ描画したい場合はとても簡単にできるが複数ピンのスタイルを変更し描画したい場合はどうすればいいのかともしかしたらなるかもしれない
これも簡単に実装できる

複数ピンの描画位置に関しては、GoogleMapReactの1階層子コンポーネントでlatやlngを元に相対位置をよしなに算出してくれるので子コンポーネントにスタイルを適用するだけで実装ができる

import * as React from 'react'
import styled from 'styled-components'
import { Color } from './brand'
import logo from './logo.svg'
import GoogleMapReact, { MapOptions, Maps } from 'google-map-react'

const App = () => {
  const pins: {
    lat: number,
    lng: number,
    name: string
  }[] = [
    {
      lat: 43.0543412,
      lng: 141.355018,
      name: 'お姉さんレーベル'
    },
    {
      lat: 43.0543451,
      lng: 141.3528293,
      name: '姉キャバ ジャックローズ'
    }
  ]
  const createMapOptions = (maps: Maps): MapOptions => {
    return {
      mapTypeControlOptions: {
        position: maps.ControlPosition.TOP_RIGHT,
      },
      mapTypeControl: false,
      zoomControl: false,
      scaleControl: false,
      streetViewControl: false,
      fullscreenControl: false,
      styles: [
        {
          featureType: 'water',
          elementType: 'geometry',
          stylers: [
            {
              color: '#e9e9e9',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'landscape',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 29,
            },
            {
              weight: 0.2,
            },
          ],
        },
        {
          featureType: 'road.arterial',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 18,
            },
          ],
        },
        {
          featureType: 'road.local',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          featureType: 'poi',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          featureType: 'poi.park',
          elementType: 'geometry',
          stylers: [
            {
              color: '#dedede',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          elementType: 'labels.text.stroke',
          stylers: [
            {
              visibility: 'on',
            },
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          elementType: 'labels.text.fill',
          stylers: [
            {
              saturation: 36,
            },
            {
              color: '#333333',
            },
            {
              lightness: 40,
            },
          ],
        },
        {
          elementType: 'labels.icon',
          stylers: [
            {
              visibility: 'off',
            },
          ],
        },
        {
          featureType: 'transit',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f2f2f2',
            },
            {
              lightness: 19,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 17,
            },
            {
              weight: 1.2,
            },
          ],
        },
      ],
    }
  }
  return (
    <Wrapper>
      <Header>
        <Logo src={logo} />
        <H1>Welcome to React</H1>
      </Header>
      <GoogleMapWrapper>
      <GoogleMapReact
        bootstrapURLKeys={{
          key: ''
        }}
        defaultCenter={{
          lat: 43.0543451,
          lng: 141.3528293
        }}
        defaultZoom={15}
        options={createMapOptions}
      >
      {
        pins.map((pin: {
          lat: number,
          lng: number,
          name: string
        }) => (
          <Pin
            lat={pin.lat}
            lng={pin.lng}
          >
          {pin.name}
          </Pin>
        ))
      }
      </GoogleMapReact>
      </GoogleMapWrapper>
    </Wrapper>
  )
}

const Wrapper = styled.div`
  text-align: center;
`
const Header = styled.header`
  background-color: ${Color.Primary};
  height: 150px;
  padding: 20px;
  color: white;
`
const Logo = styled.img`
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
  @keyframes App-logo-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
`
const H1 = styled.h1`
  font-size: 1.5em;
`
const Pin = styled.div<{
  lat: number,
  lng: number
}>`
`

const GoogleMapWrapper = styled.div`
  height: 100vh;
  width: 100%;
`

export default App

お姉さんレーベルと姉キャバ ジャックローズが表示されてることが確認できると思う
あとは、Pinコンポーネントのstyleを変更すればデザインは簡単に変更可能だ

ピン間に線を描画する

GoogleMapReactではGoogle map apiのloaded時に上乗せ描画できるapiを提供している
例えば線を描画するpolylineがある
今回はpolylineを使用しでピン間に線を引く

import * as React from 'react'
import styled from 'styled-components'
import { Color } from './brand'
import logo from './logo.svg'
import GoogleMapReact, { MapOptions, Maps } from 'google-map-react'
interface PinProps {
  lat: number,
  lng: number,
  name: string
}
const App = () => {
  const pins: PinProps[] = [
    {
      lat: 43.0543412,
      lng: 141.355018,
      name: 'お姉さんレーベル'
    },
    {
      lat: 43.0543451,
      lng: 141.3528293,
      name: '姉キャバ ジャックローズ'
    }
  ]
  const apiLoaded = (map: any, maps: any, pins: any) => {
    const path = new maps.Polyline({
      path: pins.map((p: PinProps) => ({
        lat: p.lat,
        lng: p.lng
      })),
      geodesic: true,
      strokeColor: '#356859',
      strokeOpacity: 1,
      strokeWeight: 3,
    })

    path.setMap(map)
  }
  const createMapOptions = (maps: Maps): MapOptions => {
    return {
      mapTypeControlOptions: {
        position: maps.ControlPosition.TOP_RIGHT,
      },
      mapTypeControl: false,
      zoomControl: false,
      scaleControl: false,
      streetViewControl: false,
      fullscreenControl: false,
      styles: [
        {
          featureType: 'water',
          elementType: 'geometry',
          stylers: [
            {
              color: '#e9e9e9',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'landscape',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 29,
            },
            {
              weight: 0.2,
            },
          ],
        },
        {
          featureType: 'road.arterial',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 18,
            },
          ],
        },
        {
          featureType: 'road.local',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          featureType: 'poi',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          featureType: 'poi.park',
          elementType: 'geometry',
          stylers: [
            {
              color: '#dedede',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          elementType: 'labels.text.stroke',
          stylers: [
            {
              visibility: 'on',
            },
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          elementType: 'labels.text.fill',
          stylers: [
            {
              saturation: 36,
            },
            {
              color: '#333333',
            },
            {
              lightness: 40,
            },
          ],
        },
        {
          elementType: 'labels.icon',
          stylers: [
            {
              visibility: 'off',
            },
          ],
        },
        {
          featureType: 'transit',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f2f2f2',
            },
            {
              lightness: 19,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 17,
            },
            {
              weight: 1.2,
            },
          ],
        },
      ],
    }
  }
  return (
    <Wrapper>
      <Header>
        <Logo src={logo} />
        <H1>Welcome to React</H1>
      </Header>
      <GoogleMapWrapper>
      <GoogleMapReact
        bootstrapURLKeys={{
          key: ''
        }}
        defaultCenter={{
          lat: 43.0543451,
          lng: 141.3528293
        }}
        defaultZoom={15}
        options={createMapOptions}
        onGoogleApiLoaded={({ map, maps }) => apiLoaded(map, maps, pins)}
      >
      {
        pins.map((pin: {
          lat: number,
          lng: number,
          name: string
        }) => (
          <Pin
            lat={pin.lat}
            lng={pin.lng}
          >
          {pin.name}
          </Pin>
        ))
      }
      </GoogleMapReact>
      </GoogleMapWrapper>
    </Wrapper>
  )
}

const Wrapper = styled.div`
  text-align: center;
`
const Header = styled.header`
  background-color: ${Color.Primary};
  height: 150px;
  padding: 20px;
  color: white;
`
const Logo = styled.img`
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
  @keyframes App-logo-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
`
const H1 = styled.h1`
  font-size: 1.5em;
`
const Pin = styled.div<{
  lat: number,
  lng: number
}>`
`

const GoogleMapWrapper = styled.div`
  height: 100vh;
  width: 100%;
`

export default App

お姉さんレーベルと姉キャバ ジャックローズの間に緑色の線が引かれていることが確認できるだろう

ピンをクリックすると吹き出しが表示される

GoogleMapReactは子コンポーネントのクリック時にイベント取得するonChildClickというapiを提供している
onChildClickはイベント発生時に該当するコンポーネントのkeyと子コンポーネント自体の情報(childProps)を返す

基本吹き出しを表示する際はクリックイベントのkeyとmapのindexが同じなら表示するで実装できるので、基本childPropsは使用しなくていいだろう

import * as React from 'react'
import styled from 'styled-components'
import { useState } from 'react'
import { Color } from './brand'
import logo from './logo.svg'
import GoogleMapReact, { MapOptions, Maps } from 'google-map-react'
interface PinProps {
  lat: number,
  lng: number,
  name: string
}
const App = () => {
  const [currentKey, setCurrentKey] = useState(-1)
  const pins: PinProps[] = [
    {
      lat: 43.0543412,
      lng: 141.355018,
      name: 'お姉さんレーベル'
    },
    {
      lat: 43.0543451,
      lng: 141.3528293,
      name: '姉キャバ ジャックローズ'
    }
  ]
  const apiLoaded = (map: any, maps: any, pins: any) => {
    const path = new maps.Polyline({
      path: pins.map((p: PinProps) => ({
        lat: p.lat,
        lng: p.lng
      })),
      geodesic: true,
      strokeColor: '#356859',
      strokeOpacity: 1,
      strokeWeight: 3,
    })

    path.setMap(map)
  }
  const changeBalloon = (key: string) => {
    const keyNumber = Number(key)
    if (currentKey === keyNumber) {
      setCurrentKey(-1)
    } else {
      setCurrentKey(keyNumber)
    }
  }
  const createMapOptions = (maps: Maps): MapOptions => {
    return {
      mapTypeControlOptions: {
        position: maps.ControlPosition.TOP_RIGHT,
      },
      mapTypeControl: false,
      zoomControl: false,
      scaleControl: false,
      streetViewControl: false,
      fullscreenControl: false,
      styles: [
        {
          featureType: 'water',
          elementType: 'geometry',
          stylers: [
            {
              color: '#e9e9e9',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'landscape',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 17,
            },
          ],
        },
        {
          featureType: 'road.highway',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 29,
            },
            {
              weight: 0.2,
            },
          ],
        },
        {
          featureType: 'road.arterial',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 18,
            },
          ],
        },
        {
          featureType: 'road.local',
          elementType: 'geometry',
          stylers: [
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          featureType: 'poi',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f5f5f5',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          featureType: 'poi.park',
          elementType: 'geometry',
          stylers: [
            {
              color: '#dedede',
            },
            {
              lightness: 21,
            },
          ],
        },
        {
          elementType: 'labels.text.stroke',
          stylers: [
            {
              visibility: 'on',
            },
            {
              color: '#ffffff',
            },
            {
              lightness: 16,
            },
          ],
        },
        {
          elementType: 'labels.text.fill',
          stylers: [
            {
              saturation: 36,
            },
            {
              color: '#333333',
            },
            {
              lightness: 40,
            },
          ],
        },
        {
          elementType: 'labels.icon',
          stylers: [
            {
              visibility: 'off',
            },
          ],
        },
        {
          featureType: 'transit',
          elementType: 'geometry',
          stylers: [
            {
              color: '#f2f2f2',
            },
            {
              lightness: 19,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.fill',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 20,
            },
          ],
        },
        {
          featureType: 'administrative',
          elementType: 'geometry.stroke',
          stylers: [
            {
              color: '#fefefe',
            },
            {
              lightness: 17,
            },
            {
              weight: 1.2,
            },
          ],
        },
      ],
    }
  }
  return (
    <Wrapper>
      <Header>
        <Logo src={logo} />
        <H1>Welcome to React</H1>
      </Header>
      <GoogleMapWrapper>
        <GoogleMapReact
          bootstrapURLKeys={{
            key: ''
          }}
          defaultCenter={{
            lat: 43.0543451,
            lng: 141.3528293
          }}
          defaultZoom={15}
          options={createMapOptions}
          onGoogleApiLoaded={({ map, maps }) => apiLoaded(map, maps, pins)}
          onChildClick={(key: string) => changeBalloon(key)}
        >
        {
          pins.map((
            pin: {
              lat: number,
              lng: number,
              name: string
            },
            index: number) => (
            <Pin
              lat={pin.lat}
              lng={pin.lng}
            >
            {pin.name}
            {
              index === currentKey &&
              'こんにちは'
            }
            </Pin>
          ))
        }
        </GoogleMapReact>
      </GoogleMapWrapper>
    </Wrapper>
  )
}

const Wrapper = styled.div`
  text-align: center;
`
const Header = styled.header`
  background-color: ${Color.Primary};
  height: 150px;
  padding: 20px;
  color: white;
`
const Logo = styled.img`
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
  @keyframes App-logo-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
`
const H1 = styled.h1`
  font-size: 1.5em;
`
const Pin = styled.div<{
  lat: number,
  lng: number
}>`
`

const GoogleMapWrapper = styled.div`
  height: 100vh;
  width: 100%;
`

export default App

それぞれの子コンポーネントをクリックすると"こんにちは"が表示されるだろう
onGoogleApiLoadedのmapsからgoogle apiのdraw機能を扱うことができる
参照

useStateがよくわからねえという方は自分で勉強してください

まとめ

今回使用したライブラリの使用事例の記事がなかなか見つからなかったので、今回記事にし公開することにした
これからReactでgoogle mapの使用を検討している方に少しでも参考になれば幸いだ

今回作成したサンプルはこちら

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

React + Reduxでカウントアプリを作る

はじめに

この記事では、React+Reduxを使用してカウントアプリを作ってみたいと思います。
下記が完了している前提で話を進めていくので、インストールされてない方は実施してください。

  1. Node.jsのインストール
  2. パッケージマネージャー yarnのインストール
  3. creat-react-appのインストール

参考:https://qiita.com/rspmharada7645/items/25c496aee87973bcc7a5

1. プロジェクトを作成する

まず、任意のディレクトリに移動し、create-react-appコマンドでプロジェクトを作成します。

$ create-react-app countApp

実行後、下記のようなメッセージが表示されていればOKです。

Initialized a git repository.

Success! Created countApp at /Users/******/countApp

作成したプロジェクト配下に移動し、下記のstartコマンドを実行します。

$cd countApp
$yarn start

実行後、ブラウザが起動し、以下の画面が表示されていればOKです。
スクリーンショット 2019-04-29 12.28.38.png

2. 必要なパッケージをインストールする

作成したプロジェクト配下に移動します。
今回はreduxreact-reduxの2つのパッケージを使用するので、yarnを使ってインストールします。

$cd countApp
$yarn add redux, react-redux

3. Reduxによるカウントアプリを実装する

今回は以下のようなフォルダー構成でカウントアプリを実装していきます。

.
 ├──  node_modules
 ├──  public
 ├──  src
      ├── actions
          └── index.js
      ├── components
          └── App.js
      ├── reducers
          ├── index.js
          └── count.js  
      ├── index.js        
      └──  serviceWorker.js 
 ├── index.js
 ├── yarn.lock
 ├── package.json
 └── README.md

まず、actions/index.jsを作成していきます。
ここでは、アクションの定義とアクションクリエーターを作成していきます。

今回は、カウントの値をstateで管理します。stateの状態を変更するアクションは、
カウントを増やす、減らすの2つしかないため、作成するアクションは2つになります。

actions/index.js
//Action定義
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

//Action Creater(Action Createrを呼び出すことで、stateの更新が行われる)
export const increment = () =>({
    type : INCREMENT
});

export const decrement = () =>({
    type : DECREMENT
});

次にreducers/count.jsreducers/index.jsを作成します。
count.jsの中で、実行したactionに応じて、stateの情報を更新します。

reducers/count.js
import {
    INCREMENT,
    DECREMENT,
} from '../actions';

const initialize = { value : 0 };
export default (count = initialize, action) => {
    switch(action.type){
        case INCREMENT:
            return { value : count.value + 1 };
        case DECREMENT:
            return { value : count.value  - 1 };
        default:
            return count;
    }
}
reducers/index.js
import { combineReducers } from 'redux';
import count from './count';

export default combineReducers({count});

次に、index.jsでstoreの作成とReduxをcomponentに対して利用可能にするために、ProviderCounterをラップします。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter from './components/Counter';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

import reducer from './reducers';
import * as serviceWorker from './serviceWorker';


const store = createStore(reducer);

//Provider(供給する)でラップすることで、stateの情報を共有することが可能
ReactDOM.render(
    <Provider store={store}>
        <Counter />
    </Provider>, 
    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: https://bit.ly/CRA-PWA
serviceWorker.unregister();

最後に、コンポーネントの作成をしていきます。

components/Counter.js
import React, {Component} from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from '../actions';

class Counter extends Component {
  render(){
    return (
      <React.Fragment>
        <div>
          カウント値 : {this.props.value}
        </div>
        <div>
          <button onClick={this.props.increment}>+</button>
          <button onClick={this.props.decrement}>-</button>
        </div>
      </React.Fragment>
    );
  }
}

//reduxで管理しているState情報をPropsで扱えるようにする
const mapStateToProps = state => ({ value : state.count.value});

//Action関数をPropsで扱えるようにする
const mapDispatchToProps = ({increment, decrement});

//componentとRedux Storeを接続する
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

4. カウントアプリを動かしてみよう

yarn startで動かしてみましょう!

$cd countApp
$yarn start

下記のような画面が表示されていればOKです。
+ボタンを押した時に、カウントの値が増えて、-ボタンを押した時にカウントの値が減っていればOKです。

スクリーンショット 2019-05-11 18.30.58.png

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

ReactプロジェクトがWindowsだけ動かない場合の処方箋

React Static プロジェクトを Windows でビルドしようとすると、大変不可解なクラッシュが発生しました。

$ react-static build
Bundling application for Production...

Cleaning dist...
[✓] Dist cleaned
Cleaning artifacts...

    :
    : (中略)
    :

[✓] Site Data Downloaded
Fetching Route Data...
[==========================================================] 104/104 100% 11556/s 0.0s
[✓] Route Data Downloaded (0.3s)
Exporting HTML across 4 threads...
  Error: Invariant Violation: Failed exporting HTML for URL / (../src/pages/index.tsx):   Minified React error #130; visit https://reactjs.org/docs/error-decoder.html?invariant  =130&args[]=undefined&args[]= for the full message or use the non-minified dev environ  ment for full errors and additional helpful warnings.

  - react-dom-server.node.production.min.js:10 ba
    [e-sea]/[react-dom]/cjs/react-dom-server.node.production.min.js:10:312

  - react-dom-server.node.production.min.js:11 r
    [e-sea]/[react-dom]/cjs/react-dom-server.node.production.min.js:11:166

  - react-dom-server.node.production.min.js:44 a.render
    [e-sea]/[react-dom]/cjs/react-dom-server.node.production.min.js:44:191

    :
    : (中略)
    :

  Error:

  Error: Invariant Violation: Failed exporting HTML for URL about (../src/pages/about.ts  x): Minified React error #130; visit https://reactjs.org/docs/error-decoder.html?invar  iant=130&args[]=undefined&args[]= for the full message or use the non-minified dev env  ironment for full errors and additional helpful warnings.

  - react-dom-server.node.production.min.js:10 ba
    [e-sea]/[react-dom]/cjs/react-dom-server.node.production.min.js:10:312

  - react-dom-server.node.production.min.js:11 r
    [e-sea]/[react-dom]/cjs/react-dom-server.node.production.min.js:11:166

  - react-dom-server.node.production.min.js:44 a.render
    [e-sea]/[react-dom]/cjs/react-dom-server.node.production.min.js:44:191

    :
    : (後略)
    :

他の環境で動かしてみたところ、下記のような状態でした。

  • macOS: 問題なし
  • (VirtualBox) Windows: 問題なし
  • Windows PC: クラッシュ❗️

nodeJS のバージョンがバラバラでしたので、一時的に v12.2.0 に合わせてみましたが、やはり再現します。
OS 周りの環境依存問題でしょうか?

解決策

プロジェクトルートまでのパスから シンボリックリンク を除外します。

これだけで嘘のように現象が改善しました。

その他、解決策候補

今回は複数環境の実験から、早々に OS 周りの環境依存の問題と目星をつけていたので除外していましたが、ググっている最中に出てきた解決策です。

  • react-domreact との間で、バージョン不整合が起きている可能性
    • React Hooks の絡みで、特に 16.7.x ▶︎ 16.8.x 移行時とかに起きやすいそうです。
  • export default しているのに、import { Hoge } from 形式でインポートしてしまっている

所感

Windows におけるシンボリックリンクとかジャンクションとかその辺り、不遇すぎませんか、、??

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