LoginSignup
0
5

More than 3 years have passed since last update.

React.js & Next.js超入門のチャプター6を改変:Firestore使用するようにした③

Last updated at Posted at 2020-04-14

React.js & Next.js超入門のチャプター6を改変:Firestore使用するようにした③


React.js & Next.js超入門

はなかなか良い本なのだが、最後のチャプター6「Firebaseでデータベースを使おう」でFirestoreでなくRealtime Databaseを使ってる。なので買うのを躊躇した人いるかもと思ってFirestoreを使用するコードに変えてみた。

React.js & Next.js超入門のチャプター6を改変:Firestore使用するようにした②からのつづき、
セクション6-3以降

メッセージ機能付きアドレスブックのサンプルアプリだが、オリジナルを確認してないので正解かどうかわからない。いちおう動く。備忘録として煩雑になっているコメントもそのまま残す。

データベース作成

addressコレクションを作るだけ.ドキュメントもウィザード途中で作らされるのでサンプルデータ的なものをいれておけばよい。
image.png
image.png

アプリ設計~

プロジェクト作成

P383からの説明通りだが、新規プロジェクトを作成の場合はpages/_app.js, lib/redux-store.js, static/Style.jsを作っておくことを忘れずに。

コード編集

▼リスト6-14 store.js

Firebase接続情報を.config.jsファイルに逃がすので以下のように変更

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import firebase from "firebase";
import "firebase/firestore"; // Firestore用に改変追加
import { firebaseConfig } from './.config';

// Firebase設定 ← .config.jsに分離
// Firebase初期化
var fireapp;
try {
    fireapp = firebase.initializeApp(firebaseConfig);
} catch (error) {
  console.log(error.message);
}
export default fireapp;
const db = firebase.firestore(); // Firestore用に追加
export { db }; // Firestore用に追加

// ステート初期値
const initial = {
  login:false,
  username:'(click here!)',
  email:'',
  data:[],
  items:[]
}

// レデューサー
function fireReducer(state = intitial, action) {
  switch (action.type) {
    // ダミー
    case 'UPDATE_USER':
      return action.value;
    // デフォルト
    default:
      return state;
  }
}

// initStore関数
export function initStore(state = initial) {
  return createStore(fireReducer, state,
    applyMiddleware(thunkMiddleware))
}

store.jsと同じプロジェクトルートに.config.jsを作成、以下のように。

export const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: ""
};

▼リスト6-15 Account.js

本の通り

▼リスト6-16

本の通り

▼リスト6-17 Address.js

getFireData()getItem(data)を以下のように変更

import React, {Component} from 'react';
import { connect } from 'react-redux';
import Router from 'next/router';
// import firebase from "firebase";
import Lib from '../static/address_lib';
import Account from '../components/Account';
import {db} from "../store"; // Firestore用に追加

class Address extends Component {
  style = {
    fontSize:"12pt",
    padding:"5px 10px"
  }

  constructor(props) {
    super(props);
    this.logined = this.logined.bind(this);
  }

  // login,logout処理
  logined(){
    this.getFireData();
  }
  logouted(){
    Router.push('/address');
  }


  // Firebaseからデータを取得
  getFireData(){
    if (this.props.email == undefined ||
      this.props.email == ''){ return; }
   // let email = Lib.encodeEmail(this.props.email); FirestoreではドットOK
    /* /// Firestore用に改変 ///
    let db = firebase.database();
    let ref = db.ref('address/');
    let self = this;
    ref.orderByKey()
      .equalTo(email)
      .on('value', (snapshot)=>{
        let d = Lib.deepcopy(snapshot.val());
        this.props.dispatch({
          type:'UPDATE_USER',
          value:{
            login:this.props.login,
            username: this.props.username,
            email: this.props.email,
            data:d,
            items:self.getItem(d)
          }
        });
    });
    */
    db.collection('address')
        .get()
        .then(querySnapshot => {
            const addresses = querySnapshot.docs.map(doc => doc.data());
            let d = Lib.deepcopy(addresses);
            let self = this;
            this.props.dispatch({
                type:'UPDATE_USER',
                value:{
                  login:this.props.login,
                  username: this.props.username,
                  email: this.props.email,
                  data:d,
                  items:self.getItem(d)
                }
              }); 
        })
  }

  // dataを元に表示項目を作成
  /* /// 二重ループにする必要ないので改変 ///
  getItem(data){
    if (data == undefined){ return; }
    let res = [];
    for (let i in data){
      for(let j in data[i]){
        let email = Lib.decodeEmail(j);
        let s = data[i][j]['name'];


        res.push(<li key={j} data-tag={email}
          onClick={this.go.bind(null, email)}>
          {data[i][j]['check'] == true ?
            <b>✓</b> : ''}{s} ({email})
        </li>);
      }
      break;
    }
    return res;
  }
  */
  // dataを元に表示項目を作成
  getItem(data){
    if (data == undefined){ return; }
    console.log('■getItemデータ'+JSON.stringify(data));
    let res = [];
    for (let i in data){
        let email = data[i]['email'] // 個人データのemail情報
        let s = data[i]['name'];
        console.log(i+'番目の人の■email:'+email+'name'+s);

        res.push(<li key={i} data-tag={email}
          onClick={this.go.bind(null, email)}>
          {data[i]['check'] == true ?
            <b></b> : ''}{s} ({email})
        </li>);  
      }
      return res;
  }

  // データ表示ページの移動
  go(email){
    Router.push('/address_show?email=' + email); // emailはクリックした個人のemail情報
  }

  // レンダリング
  render(){
    return (
      <div>
        <Account onLogined={this.logined}
          onLogouted={this.logouted} />
        <ul>
          {this.props.items == []
          ?
          <li key="0">no item.</li>
          :
          this.props.items
          }
        </ul>
      </div>
    )
  }
}

Address = connect((state)=> state)(Address);
export default Address;

▼リスト6-18 address_add.js

ほぼ本と同じ

import Link from 'next/link';
import Layout from '../components/Layout';

import AddressAdd from '../components/AddressAdd';
//import firebase from "firebase";

export default () =>(
  <Layout header="Address" title="address create.">
    <AddressAdd />
    <hr />
    <div>
      <Link href="/address">
        <button>back</button>
      </Link>
    </div>
  </Layout>
);

▼リスト6-19 Address.js

doAction(e)を変更。なおFirestoreではドキュメント名にドット(.)を受け付けるのでaddress_lib.jsを使用してない。

import React, {Component} from 'react';
import { connect } from 'react-redux';
import Router from 'next/router';
//import firebase from "firebase";
//import Lib from '../static/address_lib';
import Account from '../components/Account';
import {db} from "../store"; // Firestore用に追加

class AddressAdd extends Component {
  style = {
    fontSize:"12pt",
    padding:"5px 10px"
  }

  constructor(props) {
    super(props);
    if (this.props.login == false){
      Router.push('/address');
    }
    this.state = {
     name:'',
     email:'',
     tel:'',
     memo:'',
     message:'データを入力して下さい。'
    }
    this.logined = this.logined.bind(this);
    this.onChangeName = this.onChangeName.bind(this);
    this.onChangeEmail = this.onChangeEmail.bind(this);
    this.onChangeTel = this.onChangeTel.bind(this);
    this.onChangeMemo = this.onChangeMemo.bind(this);
    this.doAction = this.doAction.bind(this);
  }

  // login,logout処理
  logined(){
    console.log('logined.');
  }
  logouted(){
    Router.push('/address');
  }

  // フィールド入力処理
  onChangeName(e){
    this.setState({name:e.target.value});
  }
  onChangeEmail(e){
    this.setState({email:e.target.value});
  }
  onChangeTel(e){
    this.setState({tel:e.target.value});
  }
  onChangeMemo(e){
    this.setState({memo:e.target.value});
  }


  // データの登録処理
  doAction(e){
    let key = this.state.email;
    let data = {
      check:false, // 追加. これないとFirestoreにcheckフィールドできない
      name:this.state.name,
      email:this.state.email, // Firestore用に追加
      tel:this.state.tel,
      memo:this.state.memo
    }
    /* 
    let db = firebase.database();
    let ref = db.ref('address/'
      + Lib.encodeEmail(this.props.email) + '/'
      + Lib.encodeEmail(this.state.email));
    console.log(ref);
    ref.set(data);
    */  // Firestore用に改変↓
    const ref = db.collection('address').doc(this.props.email);
    console.log(ref);
    ref.set(data);
    this.setState({
      name:'',
      email:'',
      tel:'',
      memo:'',
      message:'※登録しました。'
    })
  }

  // レンダリング
  render(){
  return (
    <div>
      <Account self={this} onLogined={this.logined}
        onLogouted={this.logouted} />
      <hr/>
      <p>{this.state.message}</p>
      {this.props.login
      ?
      <table>
        <tbody>
          <tr>
            <th>name:</th>
            <td><input type="text" size="30"
              value={this.state.name}
              onChange={this.onChangeName}/></td>
          </tr>
          <tr>
            <th>email:</th>
            <td><input type="text" size="30"
              value={this.state.email}
              onChange={this.onChangeEmail} /></td>
          </tr>
          <tr>
            <th>tel:</th>
            <td><input type="text" size="30"
              value={this.state.tel}
              onChange={this.onChangeTel} /></td>
          </tr>
          <tr>
            <th>memo:</th>
            <td><input type="text" size="30"
              value={this.state.memo}
              onChange={this.onChangeMemo} /></td>
          </tr>
          <tr>
            <th></th>
            <td><button onClick={this.doAction}>
              Add</button></td>
          </tr>
        </tbody>
      </table>
      :
      <p>please login...</p>
      }
    </div>
    );
  }
}

AddressAdd = connect((state)=> state)(AddressAdd);
export default AddressAdd;

▼リスト6-20 address_show.js

本の通り

▼リスト6-21 AddressShow.js

いちばん難関なのだがgetAddress(email)doAction()を主に変更。
レンダリングのJSONデータから値をとってくる部分も変更。
Firestoreではフィールド名にはドット(.)が使えないのでその部分はaddress_lib.jsを使ってエンコードして保存した。

import React, {Component} from 'react'
import { connect } from 'react-redux'
//import firebase from "firebase";
import {db} from "../store"; // Firestore用に追加
import Lib from '../static/address_lib';
import Account from '../components/Account';

import Router from 'next/router';

class AddressShow extends Component {
  style = {
    fontSize:"12pt",
    padding:"5px 10px"
  }

  constructor(props) {
    super(props);
    if (this.props.login == false){
      Router.push('/address');
    }
    this.state = {
      last:-1,
      input:'',
      email:Router.query.email,
      address:null,             // state.address.docId.messages.送信者(from).timestamp.インプット内容、が実際に受信したメッセージ
      message:Router.query.email + 'のデータ' // ただ上部に表示するメッセージ
    }
    this.logined = this.logined.bind(this);
    this.doChange = this.doChange.bind(this);
    this.doAction = this.doAction.bind(this);
  }

  // login,logout処理
  logined(){
    console.log('logined');
  }
  logouted(){
    Router.push('/address');
  }


  // アドレスデータの検索
  /* オリジナルコード
  getAddress(email){
    let db = firebase.database();
    let ref0 = db.ref('address/'
      + Lib.encodeEmail(this.props.email)
      + '/' + Lib.encodeEmail(email) + '/check');
    ref0.set(false);
    let ref = db.ref('address/'
      + Lib.encodeEmail(this.props.email));
    let self = this;
    ref.orderByKey()
      .equalTo(Lib.encodeEmail(email))
      .on('value', (snapshot)=>{
        for(let i in snapshot.val()){
          let d = Lib.deepcopy(snapshot.val()[i]);
          self.setState({
            address:d
          });
          break;
        }
      });
  }
  Firestore向けに書き換え↓*/
  getAddress(email){
    db.collection('address')
        .doc(this.props.email) // 自分自身のcheckをfalseにする
        .update({check: false})

    let datas = [];
    db.collection('address')
        .where('email','==',email)
        .get()
        .then(querySnapshot => {
            const datas = querySnapshot.docs.map(doc => doc.data());
            let d = Lib.deepcopy(datas);
            console.log('■d ' + JSON.stringify(d));
            let self = this;
            self.setState({
                'address': d //address = クリックした人の個人データ詳細
            });
            console.log('■ステートのaddress ' + JSON.stringify(this.state.address));
            console.log('■ステートのaddress.name ' + JSON.stringify(this.state.address[0].name));
           // console.log('■ステートのaddress.messages ' + JSON.stringify(this.state.address[0]['atyahara@gmail_com'].messages));
        }); 
  }

  // フィールド入力
  doChange(e){
    this.setState({
      input:e.target.value
    });
  }

  // メッセージ送信処理
  doAction(){
    let from = this.props.email; // 自分自身のDocId
    let to = this.state.email; // 相手のemailを仮代入
    let d = new Date().getTime();
    let input = this.state.input;
/*
    let ref = db.ref('address/' + from + '/' + to
      + '/messages/' + d);
    ref.set('to: ' + this.state.input);
    let ref2 = db.ref('address/' + to + '/' + from
      + '/messages/' + d);
    ref2.set('from: ' + this.state.input);
    let ref3 = db.ref('address/' + to + '/' + from
      + '/check/');
    ref3.set(true);
  Firestore向けに書き換え↓*/
    db.collection('address').where('email','==',to) // to = 送信相手のemail
      .get()
      .then((querySnapshot) => {
        querySnapshot.forEach((doc) => {
          const ref = db.collection('address').doc(from); // 自分Doc
          const ref2 = db.collection('address').doc(doc.id); // 相手Doc. Snapshotからdoc.idでdocIdが取得できる
          console.log('◆自分のdoc.id '+ from)
          console.log('◆相手のdoc.id '+ doc.id);
          console.log('◆state.inputは'+ input); // 直でthis.state.inputとするとなぜかカラ.
         to = Lib.encodeEmail(doc.id); // 相手のDocIdをエンコード
         from = Lib.encodeEmail(from); // 自分のDocIdをエンコード
         let me = this.props.username;
         console.log('' + me); // me = 自分のGoogleアカウント名
          ref.update({
            [`${to}.messages.${d}`] : me + ' wrote :' + input   // 自分Doc配下に“messages/エンコード化相手アドレス/timestamp: メッセージ内容” で保存  
          })
          ref2.update({
            [`${from}.messages.${d}`] : me + ' wrote :' + input,  // 相手Doc配下に“messages/エンコード化自分アドレス/timestamp: メッセージ内容” で保存 
            check: true 
          });          
        })
      })

    this.setState({ input:''})
  }


  // レンダリング
  render(){
    if (this.state.address == null){ //address = クリックした人の個人データ詳細
      this.getAddress(Router.query.email);
    }
    let items = [];

    if (this.state.address != null){
      let from = Lib.encodeEmail(this.props.email);
      console.log('◆◆fromは '+ from);
      if (this.state.address[0][from]['messages'] != null) {
        console.log('◆◆state.address.messagesちゃん '+ JSON.stringify(this.state.address[0][from]['messages']))
        let JSONobj = this.state.address[0][from]['messages'];
        console.log('◆◆JSONobj '+ JSONobj)
        console.log('◆◆Json長さ ' + Object.keys(JSONobj).length);
        for(let item in JSONobj){ //クリックした人とのやりとりメール一覧
            console.log('◆◆'+ item + ': ' + JSONobj[item])
          items.unshift(<li key={item}>
            {JSONobj[item]}
          </li>);
        }
      } else { return; }
    }

    return (
      <div>
        <Account onLogined={this.logined}
          onLogouted={this.logouted} />
        <p>{this.state.message}</p>
        <hr/>
        {this.state.address != null
        ?
        <table>
          <tbody>
            <tr>
              <th>NAME</th>
              <td>{this.state.address[0].name}</td>
            </tr>
            <tr>
              <th>MAIL</th>
              <td>{this.state.email}</td>
            </tr>
            <tr>
              <th>TEL</th>
              <td>{this.state.address[0].tel}</td>
            </tr>
            <tr>
              <th>MEMO</th>
              <td>{this.state.address[0].memo}</td>
            </tr>
          </tbody>
        </table>
        :
        <p>no-address</p>
        }
        <hr />
        <fieldset>
          <p>Message:</p>
          <input type="text" size="40"
            value={this.state.input}
            onChange={this.doChange} />
          <button onClick={this.doAction}>send</button>
        </fieldset>
        {this.state.address != null
//         &&
//         this.state.address[0][Lib.encodeEmail(this.props.email)]['messages'] != null
        ?
        <div>
        <p>{this.state.address[0].name}さんとのメッセージ</p>
        <ul>{items}</ul>
        </div>
        :
        <p>※メッセージはありません。</p>
        }
      </div>
    );
  }

}
AddressShow = connect((state)=> state)(AddressShow);
export default AddressShow;

▼リスト6-22 address_lib.js

Firestoreのフィールド名にはドットもアスタリスク(*)も使えない。なのでアンダーバーにした。

class Lib{
  /*
  Firestoreではドット(.)の入力がOKなのでemailのエンコード・デコードいらない
  */
    static deepcopy(val){
      return JSON.parse(JSON.stringify(val));
    }
  /*
  ただ各個人のmessages配下にemailアドレスをネストするとき
  [`messages.${to}.${d}`]のようにドットで区切るのでemailのドットが邪魔になる.
  例えば,atom@yah.co.jpだとatom@yah / co / jp のようにネストされてしまう.
  そしてアスタリスク*はフィールド値に使えないときた.
 なのでアンダーバー_に変えた.
  */
    static encodeEmail(val){
      return val.split(".").join("_"); 
    }
    static decodeEmail(val){
      return val.split("_").join(".");
    }
}

export default Lib;

▼リスト6-23 Style.js

本の通り

データベースアクセスの設定

以下のように。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
    }
  }
}

ユーザー認証の設定

本の通り

スモークテスト

これでいちおう動くが…
image.png
image.png
他人(ここではアトム)をクリック
image.png
image.png
image.png
1回バックしてから再度このページに戻るとメッセージが追加されている(なぜかリアルタイムにデータを更新しない)
image.png

自分自身をクリックするとエラーなど、JSONオブジェクトの扱いで苦心。
image.png

まあこのへんで。

Firestoreデータベース構成

最終的にここまでなんとなく作り終えてやっと理解できたのだが以下のようなデータの配置にしたかったのだ、とわかった。本の通りだが、RDBのカチッとしたテーブルリレーション+スキーマというイメージにとらわれてると結構???となる。
image.png
image.png


以上

0
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
5