React + FirebaseでCRUD


React + FirebaseでCRUDのやり方をまとめようと思ってたところ、[ほぼやりたいことと同じ記事](https://www.djamware.com/post/5bc50ea680aca7466989441d/reactjs-firebase-tutorial-building-firestore-crud-web-application)があったので、そちらをなぞってみました。そのままでは動かない、あるいはWarningが出るところがあったので修正してみました。

## 準備

準備等は[上記の記事](https://www.djamware.com/post/5bc50ea680aca7466989441d/reactjs-firebase-tutorial-building-firestore-crud-web-application)を参考に。

### モジュールのインストール

firebaseを使うのでfirebase。あとはルーティングにreact-router-dom。スタイルにbootstrapを使う。

```bash
npm install --save react-router-dom bootstrap firebase
```

## オリジナルとの違い（修正点）

Warningの原因となるようなものを中心に修正。

* jsx内でのクラス指定がclassとなっていたのでclassNameに修正
* 同じくforをhtmlForに修正
* firestoreのcollectionをsubscribeしているが、unsubcribeしてなかったので追加
* firebaseの設定情報は.envに格納している
* 作成の順番をShowよりCreateを先に実装（記事が無いとShowできないので）

コードは[github](https://github.com/eizaburo/React-Firebase_CRUD)にあげてます。

## 実装（なぞるだけ）

### src/index.js

index.jsでルーティング設定をしている。
bootstrapのcssの読み込みを忘れずに。


```js:index.js
import React from 'react';
import ReactDOM from 'react-dom';

import { BrowserRouter as Router, Route } from 'react-router-dom';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';

import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import Edit from './components/Edit';
import Create from './components/Create';
import Show from './components/Show';

ReactDOM.render(
    <Router>
        <Route exact path='/' component={App} />
        <Route path='/edit/:id' component={Edit} />
        <Route path='/create' component={Create} />
        <Route path='/show/:id' component={Show} />
    </Router>,
    document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
```


### src/App.js

値を取得して表示。

```js:App.js
import React from 'react';
import { Link } from 'react-router-dom';
import './App.css';
import firebase from './Firebase';

class App extends React.Component {

    constructor(props) {
        super(props);
        this.ref = firebase.firestore().collection('boards');
        this.unsubscribe = null;
        this.state = {
            boards: [],
        }
    }

    onCollectionUpdate = (querySnapshot) => {
        const boards = [];
        querySnapshot.forEach((doc) => {
            const { title, description, author } = doc.data();
            boards.push({
                key: doc.id,
                title: title,
                description: description,
                author: author,
                // doc: doc,
            });
        });
        this.setState({
            boards: boards,
        });
    }

    componentDidMount = () => {
        this.unsubscribe = this.ref.onSnapshot(this.onCollectionUpdate);
    }

    componentWillUnmount = () => {
        this.unsubscribe();
    }

    render() {
        return (
            <div className="container">
                <div className="panel panel-default">
                    <div className="pnel-heading">
                        <h3 className="panel-title">
                            BOARD LIST
                        </h3>
                    </div>
                    <div className="panel-body">
                        <h4><Link to="/create">Add Board</Link></h4>
                        <table className="table table-stripe">
                            <thead>
                                <tr>
                                    <th>Title</th>
                                    <th>Description</th>
                                    <th>author</th>
                                </tr>
                            </thead>
                            <tbody>
                                {this.state.boards.map(board =>
                                    <tr key={board.key}>
                                        <td><Link to={`/show/${board.key}`}>{board.title}</Link></td>
                                        <td>{board.description}</td>
                                        <td>{board.author}</td>
                                    </tr>
                                )}
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        );
    }
}

export default App;
```

### src/Create.js

新規登録。バリデーションとかはしていない。

```js:Create.js
import React from 'react';
import firebase from '../Firebase';
import { Link } from 'react-router-dom';

class Create extends React.Component {

    constructor(props) {
        super(props)
        this.ref = firebase.firestore().collection('boards');
        this.state = {
            title: '',
            description: '',
            author: '',
        }
    }


    onChange = (e) => {
        const state = this.state;
        state[e.target.name] = e.target.value;
        this.setState(state);
    }

    onSubmit = (e) => {

        e.preventDefault();

        const { title, description, author } = this.state;

        this.ref.add({
            title,
            description,
            author
        })
            .then((docRef) => {


                this.setState({
                    title: '',
                    description: '',
                    author: '',
                });


                this.props.history.push("/");


            })
            .catch((error) => {
                console.error("Error adding document: ", error);
            });
    }

    render() {
        const { title, description, author } = this.state;
        return (
            <div className="container">
                <div className="panel panel-default">
                    <div className="panel-heading">
                        <h3 className="panel-title">
                            ADD BOARD
                        </h3>
                    </div>
                    <div className="panel-body">
                        <h4><Link to="/">Book List</Link></h4>
                        <form onSubmit={this.onSubmit}>
                            <div className="form-group">
                                <label htmlFor="title">Title:</label>
                                <input type="text" className="form-control" name="title" value={title} onChange={this.onChange} placeholder="Title" />
                            </div>
                            <div className="form-group">
                                <label htmlFor="description">Description:</label>
                                <textarea className="form-control" name="description" onChange={this.onChange} placeholder="Description" cols="80" rows="3" value={description}></textarea>
                            </div>
                            <div className="form-group">
                                <label htmlFor="author">Author:</label>
                                <input type="text" className="form-control" name="author" value={author} onChange={this.onChange} placeholder="Author" />
                            </div>
                            <button type="submit" className="btn btn-success">Submit</button>
                        </form>
                    </div>
                </div>
            </div>
        );
    }
}

export default Create;
```

### src/Show.js

記事を指定して表示。Edit, Deleteもここからリンク（Deleteはここでしてしまう）。

```js:Show.js
import React from 'react';
import firebase from '../Firebase';
import { Link } from 'react-router-dom';

class Show extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            board: {},
            key: '',
        }
    }

    componentDidMount = () => {
        
        const ref = firebase.firestore().collection('boards').doc(this.props.match.params.id);
        ref.get().then((doc) => {
            if (doc.exists) {
                this.setState({
                    board: doc.data(),
                    key: doc.id,
                    isLoading: false,
                });
            } else {
                console.log("No such document.");
            }
        }).catch((error) => {
            console.log(error);
        });
    }

    delete = (id) => {
        firebase.firestore().collection('boards').doc(id).delete()
            .then(() => {
                console.log("Document successfully deleted!")
                this.props.history.push("/");
            })
            .catch((error) => {
                console.log(error);
            });
    }

    render() {
        return (
            <div className="container">
                <div className="panel panel-default">
                    <div className="panel-heading">
                        <h4><Link to="/">Board List</Link></h4>
                        <h3 className="panel-title">
                            {this.state.board.title}
                        </h3>
                    </div>
                    <div className="panel-body">
                        <dl>
                            <dt>Description:</dt>
                            <dd>{this.state.board.description}</dd>
                            <dt>Author:</dt>
                            <dd>{this.state.board.author}</dd>
                        </dl>
                        <Link to={`/edit/${this.state.key}`} className="btn btn-success">Edit</Link>
                        <button onClick={() => this.delete(this.state.key)} className="btn btn-danger">Delete</button>
                    </div>
                </div>
            </div>
        );
    }
}

export default Show;
```

### src/Edit.js

特定記事の編集。

```js:Edit.js
import React from 'react';
import firebase from '../Firebase';
import { Link } from 'react-router-dom';

class Edit extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            key: '',
            title: '',
            description: '',
            author: '',
        }
    }

    componentDidMount = () => {
        const ref = firebase.firestore().collection('boards').doc(this.props.match.params.id);
        ref.get()
            .then((doc) => {
                if (doc.exists) {
                    const board = doc.data();
                    this.setState({
                        key: doc.id,
                        title: board.title,
                        description: board.description,
                        author: board.author,
                    });
                } else {
                    console.log("No such document");
                }
            })
            .catch((error) => {
                console.log(error);
            });
    }

    onChange = (e) => {
        const state = this.state;
        state[e.target.name] = e.target.value;
        this.setState({ board: state });
    }

    onSubmit = (e) => {

        e.preventDefault();

        const { title, description, author } = this.state;

        const updateRef = firebase.firestore().collection('boards').doc(this.state.key);
        updateRef.set({
            title,
            description,
            author
        })
            .then((docRef) => {
                this.setState({
                    key: '',
                    title: '',
                    description: '',
                    author: '',
                });
                this.props.history.push("/show/" + this.props.match.params.id);
            })
            .catch((error) => {
                console.log(error);
            });
    }

    render() {
        return (
            <div className="container">
                <div className="panel panel-default">
                    <div className="panel-hedding">
                        <h3 className="panel-title">
                            EDIT BOARD
                        </h3>
                    </div>
                    <div className="panel-body">
                        <h4><Link to={`/show/${this.state.key}`} className="btn btn-primary">Board List</Link></h4>
                        <form onSubmit={this.onSubmit}>
                            <div className="form-group">
                                <label htmlFor="title">Title:</label>
                                <input type="text" className="form-control" name="title" value={this.state.title} onChange={this.onChange} placeholder="Title"/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="description">Description:</label>
                                <input type="text" className="form-control" name="description" value={this.state.description} onChange={this.onChange} placeholder="Description"/>
                            </div>
                            <div className="form-group">
                                <label htmlFor="author">Author:</label>
                                <input type="text" className="form-control" name="author" value={this.state.author} onChange={this.onChange} placeholder="Author"/>
                            </div>
                            <button type="submit" className="btn btn-success">Submit</button>
                        </form>
                    </div>
                </div>
            </div>
        );
    }
}

export default Edit;
```

## おまけ

### Firebase.jsの設定を.envから取得

craete-react-appコマンドには最初からdotnevが組み込まれており、利用できるようなので、Firebase.jsファイルの各種情報を.env化してみます。

どうやら、パラメーター名にprefixとしてREACT_APP_を付けないといけないみたいです。

```js:Firebase.js
import * as firebase from 'firebase';
import firestore from 'firebase/firestore'

const firebaseConfig = {
    apiKey: process.env.REACT_APP_API_KEY,
    authDomain: process.env.REACT_APP_AUTH_DOMAIN,
    databaseURL: process.env.REACT_APP_DATABASE_URL,
    projectId: process.env.REACT_APP_PROJECT_ID,
    storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_APP_ID
};

firebase.initializeApp(firebaseConfig);

export const db = firebase.firestore();
export default firebase;
```

## 備考（課題）

極力オリジナルを変えないようにしたが、以下のような改良を加えるとよい。

* バリデーションをつける（Formik, Yup）
* CSSだけの利用ではなく、React-Bootstrap等を利用してみる