I can fly!!

地元ITベンダー→公務員→フルリモートのフリーランス Webエンジニア もうすぐ2児の父

React, Material-UI, React Redux, redux-saga, TypeScript, Firestoreを使ってチャットアプリを作りました。

せっかくの長い正月期間、だらだら過ごさないためにも「気になってた技術全部キャッチアップしてやる!!」と意気込んで取り組みました。 正月で家族が集合する中、場の空気を壊さぬよう早起きしたり、夜更かししたり、抜け出したりしながら何とか完走しました。笑 (大いに便宜を図ってくれた妻に感謝。)

成果物

チャットアプリ

ソースコード

機能

  • ユーザー名の変更
  • チャットルーム作成
  • チャットルーム一覧表示
  • 入室
  • メッセージ送受信

面白くも何ともないただのチャットアプリです。技術のキャッチアップが目的なのでアプリとしてのクオリティはめちゃ低いです。Firebase Hostingを使って公開してます。認証は未実装です。

開発環境

MacOS上にvagrantで立ち上げた仮想マシン(ubuntu16.04)で開発しました。

使用した技術

  • React
  • Material-UI
  • React Redux
  • redux-saga
  • TypeScript
  • Firestore
  • eslint, prettier

この記事で書いてること

各種ライブラリの導入手順(主にコマンド)や細かい設定。(途中からだるくなって終盤めっちゃ適当) あと、開発してみての所感など。

この記事で書かないこと

各種ライブラリの使い方、概念、思想、実装コードなどは記事の中には書いてません。 (実装部分についてはgithubにあげてるのでそちらを参照してください。)

参考URL

Ubuntuに最新のNode.jsを難なくインストールする https://reactjs.org/ https://material-ui.com/ https://create-react-app.dev/docs/adding-typescript/ https://prettier.io/docs/en/integrating-with-linters.html React × TypeScript × ESLint × Prettier 環境構築 https://react-redux.js.org/ https://stackoverflow.com/questions/35667249/accessing-redux-state-in-an-action-creator https://stackoverflow.com/questions/38544928/redux-calling-store-getstate-in-a-reducer-function-is-that-an-anti-pattern https://medium.com/@shoshanarosenfield/redux-thunk-vs-redux-saga-93fe82878b2d https://firebase.google.com/docs/web/setup?authuser=0 https://firebase.google.com/docs/web/setup?hl=ja https://firebase.google.com/docs/firestore/quickstart?hl=ja https://redux-saga.js.org/ redux-sagaで非同期処理と戦う https://github.com/redux-saga/redux-saga/blob/master/README_ja.md

所感

React Redux, redux-sagaがすごく大変でした。普段のLaravel + Vue + VuexでのSPA開発でFluxの概念には触れているので、その概念や思想の部分についてはよくわかるし理解できるのですが、実際に実装するとなると考えることが多くて。。。「技術のキャッチアップ」が目的だったので、妥協せずに公式ドキュメントを読み込んでアンチパターンを踏まないように実装しました。

Firestoreについては、無料でdatabase機能やイベントのPub/Sub機能を享受出来るし、ドキュメントも充実してるので、プロトタイプや勉強目的であれば最高に使いやすいなと思いました。でもプロダクト版に利用することを考えると、秘匿情報を守るために結局サーバを立ててFirebase Admin SDKなるものを使う必要があるので案外簡単ではないと感じます。巷では「超お手軽簡単」のイメージが強いので、この辺のセキュリティ意識が乏しい人が安易に使うと危ないかもと思いました。

TypeScriptについては、最初は「いちいち型定義するのめんどくさい。こんな調子だと日が暮れちまうよ。」って感じでした。コンパイルも全然通らんし。 でも、だんだんに慣れるし、思えば指摘がリアルタイムで丁寧だし、何よりコンパイル通りさえすればだいたいバグなく動いてくれるので、トータルで見ると開発効率上がってるんだと思います。「普段はネチネチと小言のうるさい頭カチコチめんどくさ真面目野郎だと思われてるけど、卒業間際になってそのありがたみに気づいた生徒達にめっちゃ感謝されるタイプの教師」みたいだなと思いました。

あとは副次的な効果として、1ヶ月前くらいから習得に励んでるVimがだいぶ板につきました。

今後の課題

  • reducerやsagaなど、肥大化しがちな箇所を適切な粒度で分割
  • reduxのconnectをcontainerに切り離す
  • TypeScriptの書き方を覚える(定義した型の再利用とかしたい。出来るのかな?)
  • 便利機能の導入(redux-toolkitとかredux saga firebaseとか)

まあ、ディレクトリ構成とか設計の部分になると、チーム毎プロジェクト毎に最適解が変わるのでベースの考え方だけ押さえておけるように引き続き勉強したいと思います。

以下、開発メモ

最新安定版のnodejsをインストール

aptリポジトリアップデート $ sudo apt update

nパッケージをインストールするためにnodejsとnpmインストール $ sudo apt install -y nodejs npm

nパッケージをインストール $ sudo npm install n -g

最新安定版のnodejsインストール $ sudo n stable

不要パッケージ削除

$ sudo apt purge -y nodejs npm
$ exec $SHELL -l

バージョン確認

$ node -v
v12.14.0

$ npm -v
6.13.4

reactプロジェクト作成

$ npx create-react-app chat

  • 開発用サーバ起動
$ cd chat
$ npm start
Compiled successfully!

You can now view chat in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://10.0.2.15:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

localhostの3000ポートにアクセスするとデフォルトページが確認できる。 reactのデフォページ

$ cd src
$ rm -f *

material-ui 導入

  • パッケージインストール
$ npm install @material-ui/core
$ npm install @material-ui/icons
  • font, iconファイルの読み込み 下記の2行をpublic/index.htmlのheadタグに追記する
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />``
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
  • レスポンシブ用メタタグ挿入 下記をheadタグに挿入
<meta
  name="viewport"
  content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
  • サンプルファイル作成 src/index.jsを作成する
// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Button from '@material-ui/core/Button';

function App() {
  return (
    <Button variant="contained" color="primary">
      Hello World
    </Button>
  );
}

ReactDOM.render(<App />, document.querySelector('#root'));
  • ビルド&サーバ起動 $ npm start ※このコマンド一発でサーバ起動だけじゃなくビルドもやってくれてるぽい。

ブラウザで確認すると、material-uiが適用されたボタンが表示される。 サンプル

TypeScript導入

  • パッケージインストール $ npm install --save typescript @types/node @types/react @types/react-dom @types/jest

※create-react-appの時に $ npx create-react-app my-app --template typescript これでやれば一発だったぽい。 既存のプロジェクトに導入すると移行大変そうだし、typeScript使うなら最初に入れるのが良さそう。

  • jsファイルの拡張子をtsxにリネーム まだindex.jsしかファイルないし、コーディングもしてなかったので助かった。

eslint prettier導入

  • prettierインストール eslintはreactプロジェクトにデフォルトで含まれているらしくprettierのインストールだけでいいらしい。(確かにpackage.json見るとそれっぽいのある。) $ npm i prettier eslint-plugin-prettier --dev

  • package.json編集

// package.json
...
 },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
+    "lint": "eslint src --ext .ts --ext .tsx --fix" <- この1行を追記
  },

もう一箇所

// package.json
...
// この3行を削除して、eslintrc.jsonに切り出す。
-  "eslintConfig": {  
-    "extends": "react-app"  
-  },  
  • eslintrc.jsonを作成
// eslintrc.json
{
    "extends": [
        "react-app"
    ],
    "plugins": [
        "prettier"
    ],
    "rules": {
        "prettier/prettier": "error"
    }
}

これでlinterが動くようになる。

  • linter実行 $ npm run lint eslintが静的解析、prettierがコード整形をやってくれる。

redux導入

  • モジュールインストール $ npm install --save react-redux $ npm install @types/react-redux

redux-saga導入

$ npm install --save redux-saga

Firestore導入

  • cliインストール $ sudo npm install -g firebase-tools

  • sdkインストール $ npm install --save firebase

あとはドキュメントに従ってfirebase側のセットアップを色々やる。

// firebase.json
{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "hosting": {
    "public": "build", <=ここ変更
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
  "emulators": {
    "firestore": {
      "port": 8080
    },
    "hosting": {
      "port": 5000
    }
  }
}
  • firebaseの初期化 firestoreしか使わないので、firestore()をexportして、使う先でimportする感じに実装した。
//firebaseinit.js
import * as firebase from "firebase/app";

const firebaseConfig = {
    apiKey: "AIzaSyAnFKAoPCH9LYbSD_vUBEtqmsmAnxKVpOY",
    authDomain: "react-chat-3cd68.firebaseapp.com",
    databaseURL: "https://react-chat-3cd68.firebaseio.com",
    projectId: "react-chat-3cd68",
    storageBucket: "react-chat-3cd68.appspot.com",
    messagingSenderId: "1067905229521",
    appId: "1:1067905229521:web:8cfb086a2e3bcf3dfadbf0",
    measurementId: "G-P4H92P4780"
  };

  firebase.initializeApp(firebaseConfig); 
  const db = firebase.firestore();
  export default db;
// 使う側
// api.ts
import "firebase/firestore";
import db from "./firebaseInit";
import { eventChannel } from "redux-saga";

export function fetchRooms() {
  return new Promise(resolve => {
    db.collection("rooms")
      .get()
      .then(querySnapShot => {
        let rooms: { id: string; roomName: string }[] = [];
        querySnapShot.forEach(doc => {
          rooms.push({
            id: doc.id,
            roomName: doc.data().name
          });
        });
        resolve(rooms);
      })
      .catch(() => {
        resolve([]);
      });
  });
}
~~~

以上、有意義な正月だったな〜。