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

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

<a target="_blank"  href="https://www.amazon.co.jp/gp/product/B07X7DHZ9F/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=B07X7DHZ9F&linkCode=as2&tag=kokkahasan-22&linkId=dd5df6fbbe94454250a9b9130edc29c1"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&MarketPlace=JP&ASIN=B07X7DHZ9F&ServiceVersion=20070822&ID=AsinImage&WS=1&Format=_SL160_&tag=kokkahasan-22" ></a><img src="//ir-jp.amazon-adsystem.com/e/ir?t=kokkahasan-22&l=am2&o=9&a=B07X7DHZ9F" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />
<a target="_blank" href="https://www.amazon.co.jp/gp/product/B07X7DHZ9F/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=B07X7DHZ9F&linkCode=as2&tag=kokkahasan-22&linkId=73249152ab7e0fd9cf8875e18987c25d">React.js &amp; Next.js超入門</a><img src="//ir-jp.amazon-adsystem.com/e/ir?t=kokkahasan-22&l=am2&o=9&a=B07X7DHZ9F" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />

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

[React.js & Next.js超入門のチャプター６を改変：Firestore使用するようにした②](https://qiita.com/atomyah/items/8c2f8a1e554121901d1d)からのつづき、
セクション6-3以降

メッセージ機能付きアドレスブックのサンプルアプリだが、オリジナルを確認してないので正解かどうかわからない。いちおう動く。備忘録として煩雑になっているコメントもそのまま残す。
##データベース作成
`address`コレクションを作るだけ.ドキュメントもウィザード途中で作らされるのでサンプルデータ的なものをいれておけばよい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/e3111620-e16d-7486-ada7-58b010cf263b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/434de69b-a836-c9f4-0c29-a0c228e039cc.png)

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

##コード編集
####▼リスト6-14 store.js
Firebase接続情報を.config.jsファイルに逃がすので以下のように変更

```jsx
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`を作成、以下のように。

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


####▼リスト6-15 Account.js
本の通り

####▼リスト6-16 
本の通り

####▼リスト6-17 Address.js
`getFireData()`と`getItem(data)`を以下のように変更

```jsx
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
ほぼ本と同じ

```jsx
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を使用してない。

```jsx
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を使ってエンコードして保存した。

```jsx
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のフィールド名にはドットもアスタリスク(*)も使えない。なのでアンダーバーにした。

```jsx
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
本の通り

###データベースアクセスの設定
以下のように。

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

###ユーザー認証の設定
本の通り

##スモークテスト

これでいちおう動くが…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/33efeafe-7e8e-35da-1d9a-6e3f6ac31666.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/771f0d27-2ad9-c14d-e340-8b1847cc83c2.png)
他人（ここではアトム）をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/9354f1f0-ff01-8f15-7bea-1a1b6f7fe968.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/058de37b-2ebd-b50e-059c-5c6a881bcaf1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/da477e2a-3c0c-fd6e-0002-2d99dba86e5d.png)
１回バックしてから再度このページに戻るとメッセージが追加されている（なぜかリアルタイムにデータを更新しない）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/d8624dfc-9b9a-361c-88d6-3eacabf8417d.png)

自分自身をクリックするとエラーなど、JSONオブジェクトの扱いで苦心。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/98a02eeb-5395-614b-b96c-6b7778793598.png)

まあこのへんで。

####Firestoreデータベース構成

最終的にここまでなんとなく作り終えてやっと理解できたのだが以下のようなデータの配置にしたかったのだ、とわかった。本の通りだが、RDBのカチッとしたテーブルリレーション＋スキーマというイメージにとらわれてると結構？？？となる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/0096c4c4-805f-c4fb-20dd-6b6db9323240.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/26a925f8-6ff0-51e2-34b1-d24426a984f3.png)




<br>

以上