#React.js & Next.js超入門のチャプター6を改変:Firestore使用するようにした③
はなかなか良い本なのだが、最後のチャプター6「Firebaseでデータベースを使おう」でFirestoreでなくRealtime Databaseを使ってる。なので買うのを躊躇した人いるかもと思ってFirestoreを使用するコードに変えてみた。
React.js & Next.js超入門のチャプター6を改変:Firestore使用するようにした②からのつづき、
セクション6-3以降
メッセージ機能付きアドレスブックのサンプルアプリだが、オリジナルを確認してないので正解かどうかわからない。いちおう動く。備忘録として煩雑になっているコメントもそのまま残す。
##データベース作成
address
コレクションを作るだけ.ドキュメントもウィザード途中で作らされるのでサンプルデータ的なものをいれておけばよい。
##アプリ設計~
####プロジェクト作成
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;
}
}
}
###ユーザー認証の設定
本の通り
##スモークテスト
これでいちおう動くが…
他人(ここではアトム)をクリック
1回バックしてから再度このページに戻るとメッセージが追加されている(なぜかリアルタイムにデータを更新しない)
自分自身をクリックするとエラーなど、JSONオブジェクトの扱いで苦心。
まあこのへんで。
####Firestoreデータベース構成
最終的にここまでなんとなく作り終えてやっと理解できたのだが以下のようなデータの配置にしたかったのだ、とわかった。本の通りだが、RDBのカチッとしたテーブルリレーション+スキーマというイメージにとらわれてると結構???となる。
以上