Help us understand the problem. What is going on with this article?

React+FirebaseでサーバーレスなTodoAppを作った

More than 1 year has passed since last update.

React入門をざっくり読んだので何か作ってみようと思った。
チュートリアルといえばTodoApp作成。
ただのTodoAppでは味気ないのでDB保存機能も付けようと。
じゃあ、最近イケてるっぽいサーバーレスっぽい構成にしてみるか、ということでReact+FirebaseなTodoAppを作った。

https://todo.dyoshikawa.net

かきすてTodo。
匿名ログインしてログアウトするまでの間Todoが保持される。

対象読者

  • React初心者
  • サーバレス、mBaaS初心者

使ったアレコレ

  • React
  • Firebase Hosting
  • Firebase Authentication
  • Firebase Firestore
  • Material-UI

筆者環境

  • OSX High Sierra
  • Node v9.8.0

準備

  • Node, yarn(npm)導入
  • Firebaseアカウント登録・プロジェクト作成
  • firebase-tools導入
  • ブラウザFirebaseコンソールでAuthentication, Hosting, Firestoreを使用開始状態にする

これらは割愛。
Reactプロジェクト作成はcreate-react-appでドン。

npx create-react-app my-todoapp

my-todoappをFirebaseプロジェクト化する。

cd my-todoapp
firebase init

対話式で設定する。
基本yでOKだが、プロジェクト選択は任意に今回使うものに合わせてEnter。publicはbuildと入力。
あと使用サービスを聞かれた時は、HostingとAuthenticationとFirestoreにスペースキーでチェックを入れてEnter。
僕は最初スペースで選択というのが分からず未選択のまま次項目へ行ってしまった。

Firebaseとmaterial-uiのモジュールを追加。

yarn add firebase material-ui@next

実装

JSを書いていく。

firebase.js

React初心者なのでディレクトリ構成の定番がよくわからないけれども、とりあえずsrc下にpluginsを作る。
pluginsに自前Firebaseモジュール的なものとしてfirebase.jsを書く。

plugins/firebase.js
import firebase from 'firebase';

const config = {
  apiKey: "",
  authDomain: "",
  databaseURL: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: ""
};
const firebaseApp = firebase.initializeApp(config);
export const firestore = firebaseApp.firestore();

configオブジェクトの中身はブラウザのFirebaseコンソールから確認する。
左メニューAuthenticationを選択→右上のウェブ設定をクリックすると見れるので、コピペする。
ついでに、Authenticationのログイン方法→匿名を有効にする。

App.js

App.jsを書きかえる。

Authentication関数

firebaseモジュールをimportする。

import firebase from 'firebase';

ログイン

firebase.auth().signInAnonymously()

ログイン状態を監視

firebase.auth().onAuthStateChanged(user => {
  if (user) {
    // ログイン中
  } else {
    // ログアウト中
  }
});

現在のユーザを取得

firebase.auth().currentUser

firebase.auth().currentUser.delete()関数でログアウトできる。

Firestore関数

公式ドキュメントが親切。

https://firebase.google.com/docs/firestore/manage-data/add-data
https://firebase.google.com/docs/firestore/manage-data/delete-data
https://firebase.google.com/docs/firestore/query-data/get-data

RailsとかでORM書いた経験あるとかなりとっつきやすいんじゃないだろうか。

どうやって使うかというと、さっきのfirebase.jsを読み込む。

import { firestore } from './plugins/firebase';

これでfirestore関数が使えるように・・・ならない。
consoleを見るとエラーが出ている。
ググったが理由がよくわからない。
結果的に以下を挿入すると動くようになった。

import 'firebase/firestore';

たぶん僕がimportを完全に理解できてないだけだと思う。

Material-ui

公式を参照。

https://material-ui-next.com/

雰囲気はわかるが、全部英語なので細かいところで結構詰まる。
手探りで実装した感が否めない(まあ、他の部分もそうか)。

以下、app.jsの完成形。

src/app.js
import React, { Component, Fragment } from 'react';
import firebase from 'firebase';
import 'firebase/firestore';
import { firestore } from './plugins/firebase';
import Reboot from 'material-ui/Reboot';
import List from 'material-ui/List';
import ListItem from 'material-ui/List/ListItem';
import ListItemText from 'material-ui/List/ListItemText';
import Button from 'material-ui/Button';
import TextField from 'material-ui/TextField';
import AppBar from 'material-ui/AppBar';
import Toolbar from 'material-ui/Toolbar';
import Typography from 'material-ui/Typography';
import Checkbox from 'material-ui/Checkbox';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      userId: '',
      isLogin: false,
      inputValue: '',
      tasks: []
    };
    this.getTasksData = this.getTasksData.bind(this);
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
    this.getText = this.getText.bind(this);
    this.addTask = this.addTask.bind(this);
    this.removeTask = this.removeTask.bind(this);
  }

  componentDidMount() {
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        console.log('is login');
        this.setState({
          userId: user.uid,
          isLogin: true
        });
        this.getTasksData();
      } else {
        console.log('is not login');
      }
    });
  };

  getTasksData() {
    firestore.collection('tasks')
      .where('user_id', '==', this.state.userId)
      .orderBy('created_at')
      .get()
      .then(snapShot => {
        let tasks = [];
        snapShot.forEach(doc => {
          tasks.push({
            id: doc.id,
            text: doc.data().text
          });
        });
        this.setState({
          tasks: tasks
        });
      });
  }

  login() {
    firebase.auth().signInAnonymously().then(e => {
      console.log(e);
      this.setState({
        isLogin: true,
        userId: firebase.auth().currentUser.uid
      });
    }).catch(error => {
      console.log(error.code);
      console.log(error.message);
    });
  };

  logout() {
    if (firebase.auth().currentUser == null) {
      return
    }

    firebase.auth().currentUser.delete()
      .then(() => {
        this.setState({
          isLogin: false,
          userId: '',
          inputValue: '',
          tasks: []
        });
      });
  };

  getText(e) {
    this.setState({
      inputValue: e.target.value
    });
  };

  addTask() {
    const inputValue = this.state.inputValue;
    if (inputValue === '') {
      return
    }
    firestore.collection('tasks').add({
      text: inputValue,
      created_at: new Date(),
      user_id: this.state.userId
    }).then(() => {
      this.getTasksData();
    });
    this.setState({
      inputValue: ''
    });
  };

  removeTask(e) {
    console.log(e.target.value);
    firestore.collection('tasks')
      .doc(e.target.value)
      .delete()
      .then(() => {
        this.getTasksData()
      });
  };

  render() {
    const setLoginOutButton = () => {
      if (this.state.isLogin) {
        return (
          <Button variant='raised' color='secondary' onClick={this.logout}>
            ログアウト
          </Button>
        );
      } else {
        return (
          <Button variant='raised' color='primary' onClick={this.login}>
            匿名ログイン
          </Button>
        );
      }
    };

    const setPlaceholder = () => {
      if (this.state.isLogin) {
        return 'Todo'
      } else {
        return '匿名ログインして下さい';
      }
    };

    return (
      <Fragment>
        <Reboot/>
        <AppBar position='static'>
          <Toolbar>
            <Typography type='title' color='inherit' style={{ fontSize: '20px' }}>
              かきすてTodo
            </Typography>
          </Toolbar>
        </AppBar>

        <div style={{ padding: '16px' }}>
          <div>
            {setLoginOutButton()}
          </div>

          <div>
            <List>
              {
                this.state.tasks.map(task => {
                  return (
                    <ListItem key={task.id}>
                      <Checkbox color='secondary' onClick={this.removeTask} value={task.id}/>
                      <ListItemText primary={task.text}/>
                    </ListItem>
                  );
                })
              }
            </List>
          </div>

          <div>
            <TextField placeholder={setPlaceholder()} onChange={this.getText} value={this.state.inputValue}
                       disabled={!this.state.isLogin}/>
            <Button variant='raised' color='primary' onClick={this.addTask} disabled={!this.state.isLogin}>
              追加
            </Button>
          </div>
        </div>
      </Fragment>
    );
  };
}

export default App;

サービス名がTodoなのに変数名をtasks, taskにしたのは失敗だったな。
コンポーネント化は後でやる(やらない)。

firestore.indexes.json

インデックスがない状態でwhere句を使おうとするとなぜか怒られるので設定しておく。

https://firebase.google.com/docs/firestore/query-data/indexing

公式ドキュメントにはJSON形式の書き方が載ってないっぽいのでノリで書いた。

firestore.indexes.json
{
  "indexes": [
    {
      "collectionId": "tasks",
      "fields": [
        {
          "fieldPath": "user_id",
          "mode": "ASCENDING"
        },
        {
          "fieldPath": "created_at",
          "mode": "ASCENDING"
        }
      ]
    }
  ]
}

firestore.rules

firestoreのセキュリティルールを設定する。
公式ドキュメントを参照。

https://firebase.google.com/docs/firestore/security/rules-conditions

firestore.rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /tasks/{task} {
      allow read: if request.auth.uid == resource.data.user_id;
      allow write: if request.auth.uid != null;
    }
  }
}

とりあえず、

  • ログインユーザだけがTodoを書ける
  • 自分のTodoが他人に見られないようにする

と設定したつもり。
間違ってたら指摘お願いします。

デプロイ

yarn build && firebase deploy

感想

なんかほとんどFirebaseの記事になってしまった。

VPS借りてNginxやらRoRやらPHPやらMySQLやら環境構築して〜というバックエンドの部分から開放されるのは確かに楽。
人的リソースの少ない趣味開発やスピード命のスタートアップではガンガン使っていくべきなんじゃないだろうか(僕が言うまでもなく使ってるんだろうけど)。

今回はReact単体で作ったけど、RouterとかReduxにも慣れていきたい。
サーバーレスは同じFirebaseのCloud Functionsか、AWS Lambda, DynamoDB方面にも手を出してみようか。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away