会社の忘年会で使うブラウザビンゴゲームをReactで作ってみた

## はじめに
掲題の通り。
色々と雑ですが、作ってみて感じた事等、以下にまとめたいと思います。

## できたもの
[**こんなの**](https://bingogame-ee193.firebaseapp.com/) 
![konnano.gif](https://qiita-image-store.s3.amazonaws.com/0/119363/ebed1587-0dc5-0c3c-61d0-01b0b7d66642.gif)


## 経緯諸々…
- **会社の忘年会で景品を賭けたゲームをやりたい。**
 - オーソドックスにビンゴで行きましょう( ･ㅂ･)و ̑̑
 - 会場にはプロジェクタがあるので、PCで利用出来るビンゴアプリでやりましょう( ･ㅂ･)و ̑̑
 - カスタムしたい機能(後述)もあるので、いっそ作ってしまいましょう( ･ㅂ･)و ̑̑
 - **じゃあwebアプリで!** ( 筆 者･ㅂ･)و ̑̑  　**reactで!** ( 筆 者･ㅂ･)و ̑̑  　**僕が作りますので!** ( 筆 者･ㅂ･)و ̑̑ 

## 要件
- 普通のビンゴ機能
 - ボタンクリックで1～75の数値をランダムに抽選出来る
 - 当選した数が分かるように常に表示する(過去に当選した数も)
 - ビンゴ結果のクリア(初期化)が出来る
 - もってくPCはchrome入りのwin10だから、とりあえずchromeで動けばよい
- **カスタム要件①**
 - **「enterキー押下」でもビンゴの抽選が出来る様にする**
     -  参加者一同が注目する中で抽選するため、**ボタン押した感**を出したかった
- **カスタム要件②**
 - **最小値と最大値を自由に変更出来る様にする**
     -  目玉景品(Nintendo switch)はビンゴゲームとは別に、**1発勝負の抽選で当選者を決めたい**

## ざっくりシステム構成
**ライブラリ** : react 16.1.1
**UIデザイン** : material-ui 0.20.0
**バンドラー** : webpack 3.6.0
**トランスパイラ** : babel(-core) 6.26.0
**ホスティング** : firebase hosting

## な ぜ react に し た の か
- ぶっちゃけると、最近やっとビギナー程度には書ける様になった(と思いたい)reactでのコーディングが楽しくしょうがないからという、極めて個人的な理由~~エゴ~~でreactを選びました
 - 業務で使った事は一度も無い(~~使っても好きでいられるだろうか~~)
 - 今回は業務という訳ではないのでアーキテクチャ諸々は僕の一存!調子に乗りました。すみません。
 - 反省点(後述)でも述べますが、正直これくらいの要件ならreactを使わない方が適切だったのかな・・・とも思い返しました:disappointed_relieved:
 - ~~それでもとにかくreactで書きたかった~~

## アプリ構成
```
　├ resources/
　│　├ bundle.js
　│　└ index.css
　├ src/
　│　├ component/
　│　│　├ BingoNumber.jsx
　│　│　├ BingoNumbers.jsx
　│　│　├ ConfigArea.jsx
　│　│　├ ConfigButton.jsx
　│　│　└ DoButton.jsx
　│　├ service/
　│　│　└ BingoDrawer.js
　│　├ utils/
　│　│　└ ArrayExtension.js
　│　└ app.js
　└ index.html # endpoint
```
見返してみると、ポリシーもへったくれもないものすごく適当な構成になってしまっていて、改めて自分のコード設計力の無さを痛感しました。
(reactに限った話ではありませんが)適切にコンポーネント化やクラス分けが出来る様なスキルを身に着けたいです。。。
　~~「BingoNumber」と「BingoNumbers」って何…?なんでそれぞれ別れてるの?とか・・・~~
　~~[単一責任の原則](https://code.tutsplus.com/ja/tutorials/solid-part-1-the-single-responsibility-principle--net-36074)?何それおいしいの?とか・・・~~

## メイン処理(抜粋)
```js
const BINGO_MIN_NUMBER = 1
const BINGO_MAX_NUMBER = 75
const KEY_CODE_ENTER = 13

class BingoGame extends React.Component {
  constructor() {
    super()

    this.state = {
      currentNumber : <BingoNumber size="big" isHit={true} value={0}/>,
      nowDrawing : false,
      doButton : <DoButton label="start" onClick={::this.startDrawing} />,
      bingoDrawer : new BingoDrawer({ min : BINGO_MIN_NUMBER, max : BINGO_MAX_NUMBER }),
      hitNumbers : [],
      isOpenConfig : false,
      max : BINGO_MAX_NUMBER,
      min : BINGO_MIN_NUMBER,
      reSettingMax : BINGO_MAX_NUMBER,
      reSettingMin : BINGO_MIN_NUMBER,
      reSettingMaxError : '',
      reSettingMinError : ''
    }
  }

  componentWillMount() {
    document.body.addEventListener('keydown', ::this.onEnterKeyDown)
    document.body.addEventListener('keyup', ::this.onEnterKeyUp)
  }

  onEnterKeyDown(e) {
    if(KEY_CODE_ENTER === e.keyCode) {
      document.activeElement.blur()
    }
  }

  onEnterKeyUp(e) {
    if(KEY_CODE_ENTER !== e.keyCode) {
      return
    }

    if(this.state.nowDrawing) {
      this.finishDrawing()
    } else {
      this.startDrawing()
    }
  }

  componentDidUpdate() {
    this.state.doButton = this.state.nowDrawing ?
                            <DoButton label="stop" onClick={::this.finishDrawing} /> :
                            <DoButton label="start" onClick={::this.startDrawing} />
  }

  isFinished() {
    return this.state.hitNumbers.length >= ((this.state.max - this.state.min) + 1)
  }

  startDrawing() {
    if(this.state.nowDrawing) {
      return
    }

    if(this.isFinished()) {
      return
    }

    this.state.nowDrawing = setInterval(() => {
      const randomNumber = Math.floor(Math.random() * (this.state.max - this.state.min + 1) + this.state.min)
      this.setState({
        currentNumber : <BingoNumber size="big" isHit={true} value={randomNumber}/>
      })
    }, 85)
  }

  finishDrawing() {
    if(!this.state.nowDrawing) {
      return
    }

    clearInterval(
      this.state.nowDrawing
    )

    const hitNumber = this.state.bingoDrawer.draw()
    const hitNumbers = this.state.hitNumbers
    hitNumbers.push(hitNumber)

    this.setState({
      nowDrawing : false,
      doButton : <DoButton label="start" onClick={::this.startDrawing} />,
      currentNumber : <BingoNumber size="big" isHit={true} value={hitNumber}/>,
      hitNumbers
    })
  }

  resetAll() {
    const shouldReset = confirm('抽選結果をすべてクリアします。よろしいですか?')
    if(shouldReset) {
      clearInterval(
        this.state.nowDrawing
      )

      this.setState({
        currentNumber : <BingoNumber size="big" isHit={true} value={0}/>,
        nowDrawing : false,
        doButton : <DoButton label="start" onClick={::this.startDrawing} />,
        bingoDrawer : new BingoDrawer({ min : this.state.min, max : this.state.max }),
        hitNumbers : [],
        reSettingMaxError : '',
        reSettingMinError : ''
      })
    }
  }

  toggleConfigArea() {
    this.setState({ isOpenConfig : !this.state.isOpenConfig })
  }

  syncValue(e) {
    const changedState = {}
    changedState[e.target.name] = e.target.value

    this.setState(changedState)
  }

  clearConfigErrorMessage() {
    this.setState({
      reSettingMaxError : '',
      reSettingMinError : ''
    })
  }

  saveConfigAndRestart() {
    this.clearConfigErrorMessage()

    const reSettingMax = +this.state.reSettingMax
    const reSettingMin = +this.state.reSettingMin

    const validateTargets = [
                              {
                                target : 'min',
                                value : reSettingMin
                              },
                              {
                                target : 'max',
                                value : reSettingMax
                              }
                            ]

    const isNanError = ((validateTargets, _this) => {
      let result = false
      validateTargets.forEach(props => {
        if(!props.value || isNaN(props.value) || 0 > props.value) {
          const errorMessage = {}
          errorMessage[`reSetting${props.target.charAt(0).toUpperCase() + props.target.slice(1)}Error`] = `${props.target}には1以上の半角数値を入力してください`
          _this.setState(errorMessage)
          result = true
          return
        }
      })
      return result
    })(validateTargets, this)

    if(isNanError) {
      return
    }

    if(reSettingMin >= reSettingMax) {
      this.setState({ reSettingMaxError : 'minより大きい値を入力して下さい' })
      return
    }

    clearInterval(
      this.state.nowDrawing
    )

    this.setState({
      currentNumber : <BingoNumber size="big" isHit={true} value={0}/>,
      nowDrawing : false,
      doButton : <DoButton label="start" onClick={::this.startDrawing} />,
      bingoDrawer : new BingoDrawer({ min : reSettingMin, max : reSettingMax }),
      hitNumbers : [],
      max : reSettingMax,
      min : reSettingMin,
      isOpenConfig : !this.state.isOpenConfig,
      reSettingMaxError : '',
      reSettingMinError : ''
    })
  }

  render() {
    return (
      <div>
        {this.state.doButton}

        <ConfigButton
          onClick={::this.toggleConfigArea}
        />
        <ConfigArea
          onRequestChange={::this.toggleConfigArea}
          open={this.state.isOpenConfig}
        >
          <TextField
            hintText="Min"
            floatingLabelText="Min"
            style={{ marginLeft:'1vw' }}
            defaultValue={this.state.min}
            onChange={::this.syncValue}
            name="reSettingMin"
            errorText={this.state.reSettingMinError}
            underlineShow={false} />
          <TextField
            hintText="Max"
            floatingLabelText="Max"
            style={{ marginLeft:'1vw' }}
            defaultValue={this.state.max}
            onChange={::this.syncValue}
            name="reSettingMax"
            errorText={this.state.reSettingMaxError}
            underlineShow={false} />
          <RaisedButton
            label="save & reStart"
            primary={true}
            icon={<i className="material-icons">refresh</i>}
            fullWidth={true}
            onClick={::this.saveConfigAndRestart}
          />
          <RaisedButton
            label="Secondary"
            secondary={true}
            label="close"
            icon={<i className="material-icons">close</i>}
            style={{ marginTop : '4.0vh' }}
            fullWidth={true}
            onClick={::this.toggleConfigArea}
          />
        </ConfigArea>

        <button
          type="button"
          className="button-clear"
          onClick={::this.resetAll}
          >
          Clear
        </button>

        <div className="main">
          {this.state.currentNumber}
        </div>

        <BingoNumbers
          min={this.state.min}
          max={this.state.max}
          hitNumbers={this.state.hitNumbers}
        />
      </div>
    )
  }
}
```
「読むだけでアプリの大体の動きが想像出来る様なソースコードを!」という理想を胸に抱いて作りましたが、最終的にはエディタの上下移動がキツくて、何をやっているのかも伝わりにくいコードになってしまいました・・・~~センス×~~

## 四苦八苦した所
- これどうやるんだろう・・・  
![kore.gif](https://qiita-image-store.s3.amazonaws.com/0/119363/5daced23-1bbf-b7fc-56ee-bc5eccc526d6.gif)
 - **setIntervalでBingoNumberコンポーネントをrenderさせまくる**  

```js:app.js
this.state.nowDrawing = setInterval(() => {
  const randomNumber = Math.floor(Math.random() * (this.state.max - this.state.min + 1) + this.state.min)
  this.setState({
    currentNumber : <BingoNumber size="big" isHit={true} value={randomNumber}/>
  })
}, 85) // マジックナンバー…


// 止める時
clearInterval(
  this.state.nowDrawing
)
```  
処理中はrenderが走り続けるため、パフォーマンス的にどうなんだろう?と疑問に思いましたが、特に影響はありませんでした。~~react fiberによる恩恵?~~  

## 反省点諸々
- reactじゃなくて良かったよね
 - ビンゴゲーム終了後には、githubでソースを見た直属の上司から『なんでreactにしたん?jQueryとes6のクラス構文でも出来たじゃん(これくらいの要件ならその方が速くね?)。お前ただ覚えたてのreact使いたかっただけだろ』と、ぐうの音も出ない大正論を言われ何も言い返せませんでした。~~そうです。ただreactが書きたかっただけなんです。~~普段の業務ではこのreact熱を出しすぎない様に、平時はもうちょっと冷静で居る様心がけます
- cssの管理がすごく適当
 - ロジックを書いて満足し、反面スタイルシートは一元で管理して、変更があれば都度手を加え・・・という、そもそも何がベストかも分かっていない自分からしても『適当だな～』と思う適当さ加減。reactの初歩的なお作法はそこそこ頭に入ってきたのかな、と思うので、次はcssの管理からも目を背けずに開発しようと思います
 - ~~ほぼDrawer用にして、非常に中途半端な使い方をしてしまったmateriul-uiくん・・・~~
- ていうか番号可変なら『ビンゴゲーム』っていう名前は不適t(ry

## 成果物
ホスティング中 : [https://bingogame-ee193.firebaseapp.com/](https://bingogame-ee193.firebaseapp.com/)
ソースコード : [https://github.com/nishiurahiroki/bingo-game-made-with-react](https://github.com/nishiurahiroki/bingo-game-made-with-react)