政治ハック：2017年東京都議会議員選挙の候補者情報サイトをReact  + Material-UIで構築



## はじめに

この記事は、[政治ハック：2017年東京都議会議員選挙の候補者情報をJSONデータ化する](http://qiita.com/mshk/items/73d3d184ea9b81f0c86e)の後編です。前編で作成したJSONデータを元に、React + Material-UIでサイトを構築、Github Pagesにデプロイするまでを解説します。

ソースコード:
https://github.com/mshk/togisen2017

ウェブサイト:
https://mshk.github.io/togisen2017/

## 技術スタック

今回使用したツールは下記の通りです。

* [Create React App](https://github.com/facebookincubator/create-react-app)
* React
* Redux
* [React Router](https://github.com/ReactTraining/react-router)
* [Material-UI](http://www.material-ui.com/#/)
* Github Pages

選挙情報のサイトは、前の記事でも書いたように制作の期間が短くなりがちで、しかも選挙当日に稼働していないと意味をなさないため、サーバの構築やモニタリングなどは可能な限り避ける必要があります。そのため、スタティックなファイル配信のみでインタラクティブなサイトを構築できるReactを採用し、Single Page Applicationとして実装、デプロイ先も自分でサーバの管理をする必要がないGithub Pagesを使用しました。

## Create React Appで骨組みを構築する

Create React AppはFacebookが提供のいわゆるボイラープレート・ツールです。

`````
> npm install create-react-app -g

> create-react-app togisen2017
> cd togisen2017
> npm start
`````
でひとまず動作するReactプロジェクトを生成できます。

```
> npm run build
```
を実行すると`build`ディレクトリにデプロイ用のHTML+JavaScriptがダンプされます。この`build`ディレクトリをそのままスタティック配信すればサイトが動作します。

## React Routerを使ったルーティング

ページ遷移を実現するため、React Routerを使用しました。

```js:App.js
<Router 
 basename={'/togisen2017'}
 {...props}
>
  <Switch>
    <Route exact path="/" component={Candidates} />
    <Route path="/data" component={Data} />                  
    <Route path="/about" component={About} />
    <Route component={NotFound} />
  </Switch>
  (省略)
</Router>
```

今回のプロジェクトでは、タイトルバーとドロワーをアプリケーション全体で描画し、`Switch`で囲んだボディの部分にそれぞれのページをレンダリングするコンポーネントを差し込む形式で記述しています。

## Reduxでデータ管理

先の記事で制作したJSONデータに`Redux`を通じてアクセスします。Actionの中でHTTPクライアント`axios`を使ってHTTP経由でJSONを取得しています。今回のサイトは動的にデータを変更する機能がないため、JSONデータをwebpackでコンパイル時にJSに含めてしまい、単純に`require data from '../candidates.json'`のようにしても良かったのですが、将来的に結果にフィルターをかけるなどの拡張を行うためにこのようにしました。後からGithub Pagesへのデプロイ時に苦労することになったので、正直どうだったのか？という気持ちはありますが...

## Material-UIでUIを構築

UIの構築にはMaterial-UIを使用しました。Googleの提唱するMaterial DesignのUIパーツをReactコンポーネントとして提供しています。サイトのギャラリーをチェックして、今回のサイトで使用するUIパーツが揃っていて、Google検索の結果でも良く利用されている様子だったので使用しました。レスポンシブに対応していて、デスクトップとモバイルに同じコードで対応出来る点も決め手になりました。

結果としては、だいたいこんな感じ？と思う通りに動作し、ドキュメントも非常に分かりやすかったので助かりました。サイト独自のUIを追求したい場合のカスタマイズの柔軟度については分かりませんが、また使ってみたいコンポーネントです。

* ページトップのナビゲーション・バーに`AppBar`
* メニューボタンを押した時に横から出てくるメニューに`Drawer`と`Menu`
* それぞれの候補者の情報の表示に`Card`

を利用しています。

## Cardコンポーネントの「エキスパンド」機能を使って表示する情報を整理する

![スクリーンショット 2017-07-04 18.36.51.png](https://qiita-image-store.s3.amazonaws.com/0/9502/e6a4dc52-1f46-a0b6-072e-81c4e06c2a79.png)


`Card`コンポーネントは、ヘッダー部分にプロフィール画像を表示するなど、定番フォーマットが提供されていますのでこれをそのまま使っています。また、選挙の候補者の情報ということで、一覧では選挙区のみを表示し、ユーザーが自分の選挙区をタップするとさらに候補者の一覧が表示される「アコーディオン・メニュー」方式にしています。この「エキスパンド」の仕組みも、`Card`コンポーネントが標準で提供しているものです。

```js:components/Candidates/area.js
<Card key={this.props.area}>
  <CardHeader
    title={this.props.area}
    showExpandableButton={true}
    actAsExpander={true}
  />
  <CardText expandable={true}>
    {candidates}
  </CardText>
</Card>
```

## Drawerをモバイル／デスクトップ両対応にする

前述のように、Material-UIはレスポンシブに対応したライブラリなのですが、なぜかDrawerの幅については「%で指定する」というAPIになっています。両方のデバイスでちょうどよい値を設定することは難しいため、`isMobile`を判定して幅を切り替えています。

```js:App.js

const isMobile = window.matchMedia("only screen and (max-width: 760px)")

render() {
(省略)
  <Drawer
    docked={false}
    width={ isMobile.matches ? '80%' : '30%'}
    open={this.state.open}
    onRequestChange={(open) => this.setState({ open })}
  >
}
```

## AppBarのmarginを削除する

小ネタですが、Create React Appの生成するプロジェクトにMaterial-UIを使用すると、画面上部のナビゲーション・バーにマージンがつくという問題が発生しました。時間もないので、`public/index.html`のbodyタグに直接`style="margin: 0"`をつけるという荒業で対応しました。

## Github Pagesへのデプロイ

ここまでは順調だったのですが、すっかり忘れていたのがGithub Pagesでホスティングをする際のURLの形式です。今回は独自ドメインを使用しなかったのですが、URLが`https://mshk.github.io/togisen2017`とサブパスがルートとなります。この場合、画像やJSONのパスが開発環境とことなるため、いくつか考慮が必要です。

* 画像はwebpackのimportの仕組みを使う
* Redux StoreでAPIアクセスに使用する`axios`を初期化する際に`baseURL`を指定する

という対応をしています。

```js:actions/index.js
import Axios from 'axios'

let axios = Axios.create({
  baseURL: '/togisen2017/'
})
```

## Github PagesにSPAをデプロイする際のルーティング問題

このサイトはスタティック配信のみによるSPAとなっていますが、このプロジェクトをGithub Pagesにデプロイする際にサブパスのルーティングをどうするのか？という問題があります。Github Pagesはサブパスを「ファイルパス」として認識するからです。自前でサーバを立てる場合は、ルーティングを細く設定することができるのですが、今回はそうもいきません。

* ルートパスへのリクエストには`index.html`の内容が返される
* 「/assets/xxx.js」などはそのパスに存在するファイルの内容が返される
* それ以外は404

というシンプルなルーティング・ルールになっていて、どうしたものか？と思っていたのですが、「404.html」をカスタマイズできる事に気づき、では404.htmlにindex.htmlの内容をコピーすれば良いのでは？と思い試してみたところ、さくっとうまく行きました。React Routerで独自に404を返す処理を実装しているので、アセットとしても機能としても存在しないパスにも対応できます。

かなりチートぽいですが、ひとまず動いているということで、同じようにGithub PagesでSPAを配信する方は試してみて下さい。あるいは、もっとちゃんとした解決方法があれば教えてください。

index.htmlの内容は、`npm run build`をする度に(ロードするJSのファイル名が)変わるので、`packages.json`の`script`でReactプロジェクトのビルドを行い、その後に`index.html`を`404.html`にコピーする様にしています。

## Reactで動的に生成したページはGoogleにインデックスされるのか？問題

このサイトのように、空のindex.htmlを配信して、後からReactで動的にコンテンツをレンダリングする場合に気になるのは「Googleのクローラーはちゃんとインデックスしてくれるのだろうか？」という点です。2016年に書かれた以下の記事ではReactで構築したサイトを[Fetch as Google]でテストしていて、この時点では「Reactは問題なかったけど、React Routerを入れたらインデックスされなくなった」という結論が書かれていました。

[Testing a React-driven website’s SEO using “Fetch as Google” ](https://medium.freecodecamp.org/using-fetch-as-google-for-seo-experiments-with-react-driven-websites-914e0fc3ab1)

今回React Routerを試したのは、その辺の検証も目的だったのですが、結論としては現在のバージョンでは問題なくインデックスされていました。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">React Router入れてみたけど、いまのところFetch as Googleで問題なくレンダリングできてる。まあでも、こうやっていちいち確認しないといけないのは辛いからserver side renderingを導入するのが妥当とは言える <a href="https://t.co/zY3tEW4yU8">https://t.co/zY3tEW4yU8</a></p>&mdash; 立薗理彦 (@mshk) <a href="https://twitter.com/mshk/status/879890200633122816">2017年6月28日</a></blockquote>

## 終わりに

駆け足ですが、Reactを使ったSingle Page Applicationの構築と、そのビルド内容をGithub Pagesで配信する方法について書きました。参考になれば幸いです。