【超簡単】React×Firebaseで投稿機能作ってみた〜

### やること
ReactとFirebaseを使って超簡単な投稿アプリを作ります！
![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/8e930c92-3afb-3a32-e2ab-90285db8bd44.gif)

### Reactアプリの作成
```terminal:terminal
npx create-react-app post-app
cd post-app
npm install firebase
npm install -g firebase-tools
```

srcフォルダ内に`AllPosts.js`と`NewPost.js`ファイルを作成（コンポーネント）
```javascript:src/AllPosts.js
import React from 'react'

const AllPosts = () => {
    return (
        <>
            <p>投稿一覧</p>
        </>
    )
}

export default AllPosts
```
```javascript:src/NewPost.js
import React from 'react'

const NewPost = () => {
  return (
    <>
        <p>新規投稿</p>
        <form>
            <input type="text" placeholder='タイトル' />
            <input type="text" placeholder='内容' />
            <button type='submit'>投稿</button>
        </form>
    </>
  )
}

export default NewPost
```
```javascript:src/App.js
import './App.css';
import NewPost from './NewPost';
import AllPosts from './AllPosts';

function App() {
    return (
        <div className="App">
            <NewPost />
            <AllPosts />
        </div>
    );
}

export default App;
```
```terminal:terminal
npm start
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/4fb684b2-c86f-ee4f-5095-5dda59eb3d03.png)
こんな感じになってればおけです！

### firebase側の設定
[このリンク](https://console.firebase.google.com/u/0/)にアクセスする
ログインしといてください！！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/6bcfe2fb-2c2e-e70b-aa8b-8c4928c01ca1.png)
「プロジェクトを追加」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/ae645f9b-0087-1ca3-b0be-0bdd456e1040.png)

とりあえず「続行」押しまくってプロジェクト作成すればオッケーです！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/cf2ef253-5eaf-32b1-3f0c-d259f264d9f0.png)
こんな感じのとこに行ってれおけです！！
![Screenshot 2022-12-22 10.25.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/33f1dcd9-8714-6011-5862-2da0dcca89b1.png)
写真のとこ`</>`クリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/5a65ce8d-6062-e6a0-8dce-34708cbffaa3.png)
![Screenshot 2022-12-22 10.28.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/fbb002c6-451a-ebfb-a7b6-f99b7fa3ea9f.png)
写真のとこにAPIkey等いろいろ書いてあるのでコピーしといてください

コピーしたものはsrcフォルダに`firebase.js`というファイルを作成して貼っつけときましょー
```javascript:src/firebase.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
  apiKey: "**********",
  authDomain: "**********",
  projectId: "**********",
  storageBucket: "**********",
  messagingSenderId: "**********",
  appId: "**********",
  measurementId: "**********"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
```

次に、
![Screenshot 2022-12-22 10.34.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/9648da54-0e66-6944-c8c3-581a22b3c13d.png)
「構築」の中の「Firebase Database」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/a0e9a862-029c-9c2d-6ee4-70b302cc817f.png)
「データベースの作成」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/75f62e5b-9cf0-e4b9-2f2a-cea0be51ee78.png)
テストモードで開始
そのまま有効にするで大丈夫です
![Screenshot 2022-12-22 10.38.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/e31168ab-608f-0105-96eb-ab4c89401944.png)
次にルールを少し書き換えます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/68cafbc1-1c1b-9f3f-e334-c3873e33e763.png)
写真の選択している箇所を消します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/17a3e049-fe23-03ae-8d77-12fc08ba9dd7.png)
「公開」をクリックします
ここまでで、firestore側の設定は終了です！
### React側書いていくぞおお！！
ここからはreactで先ほどコピーしたAPIkeyを使って投稿機能を作っていきます！
```javascript:src/firebase.js
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "**********",
  authDomain: "**********",
  projectId: "**********",
  storageBucket: "**********",
  messagingSenderId: "**********",
  appId: "**********",
  measurementId: "**********"
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export default db;
```

まずは、NewPostからいきましょー
```javascript:NewPost.js
import { useState } from "react";
import { collection, addDoc } from "firebase/firestore";
import db from "./firebase";

const NewPost = () => {
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    const onSubmit = async (e) => {
        e.preventDefault();
        try {
            await addDoc(collection(db, "posts"), {
                title:title,
                content:content,
                created_at:new Date().getTime()
            });
            setTitle('')
            setContent('')
        } catch (error) {
            console.log(error);
        }
    };
    return (
        <>
            <p>新規投稿</p>
            <form onSubmit={onSubmit}>
                <input
                type="text"
                value={title}
                placeholder='タイトル'
                onChange={(e) => setTitle(e.target.value)}
                />
                <input
                type="text"
                value={content}
                placeholder='内容'
                onChange={(e) => setContent(e.target.value)}
                />
                <button type='submit'>投稿</button>
            </form>
        </>
    )
}

export default NewPost
```
解説
```
const onSubmit = async (e) => {
    e.preventDefault();
    try {
        await addDoc(collection(db, "posts"), {
            title:title,
            content:content
        });
    } catch (error) {
        console.log(error);
    }
};
```
このonSubmit関数は投稿ボタン押された時にfiresoreへデータを書き込む処理をしています

preventDefaultは、イベントに対するデフォルトの動作を止めるメソッドです。
async-awaitは非同期処理をしています
collection（コレクション）はSQLのテーブルのようなもので、ドキュメントがレコードのようなものです
addDocは引数に指定しているコレクションにドキュメントを追加する関数です
dbはsrc/firebase.jsからインポートしています
（useStateについてわからない人はググったらいっぱい出てくるので解説は省略します:bow_tone2:）
今回はpostsコレクションに、useStateで指定しているtitleとcontentを追加しています
一応省略できます（今回は辞書型であることを意識するために長い方で書いてます）
```
const onSubmit = async (e) => {
    e.preventDefault();
    try {
        await addDoc(collection(db, "posts"), {
            title, //省略
            content //省略
        });
    } catch (error) {
        console.log(error);
    }
};
```
ここで一旦サイトを立ち上げてみましょう
```terminal:terminal
npm start
```
特に見た目は変わっていないと思いますが、入力フォームに適当に入力して投稿してみましょう
まだ表示部分は作っていないので、表示はされませんが、firebaseのサイトに移動するとデータが書き込まれていることがわかります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/5d61c2bc-0895-881f-aa79-21b25e8a57f2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/068ad80a-82cb-d6b8-b3e3-60346a4c2f08.png)
投稿の一覧表示していきます
```javascript:AllPosts.js
import { collection, onSnapshot } from 'firebase/firestore'
import React, { useEffect, useState } from 'react'
import db from './firebase'

const getStrTime = (time) => {
    let t = new Date(time);
    return (`${t.getFullYear()}/${t.getMonth()+1}/${t.getDate()} ${t.getHours()}:${t.getMinutes()}:${t.getSeconds()}`);
}

const AllPosts = () => {
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        onSnapshot(collection(db, 'posts'), (posts) => {
            setPosts(
                posts.docs.map((post) => post.data()).sort((a,b) => b.created_at - a.created_at)
            )
        })
    }, []);

    return (
        <>
            <p>投稿一覧</p>
            {posts.map((post) => (
                <div className='post'>
                    <div className='title'>タイトル：{post.title}</div>
                    <div className='content'>内容：{post.content}</div>
                    <div className='created_at'>投稿日：{getStrTime(post.created_at)}</div>
                </div>
            ))}
        </>
    );
}

export default AllPosts
```
解説
```
const getStrTime = (time) => {
    let t = new Date(time);
    return (`${t.getFullYear()}/${t.getMonth()+1}/${t.getDate()} ${t.getHours()}:${t.getMinutes()}:${t.getSeconds()}`);
}
```
この関数は`new Date().getTime()`で取得した数字を表示用の文字列に変換しています
`new Date().getTime()`で取得した数字↓↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/c246da91-aaec-c7b8-cf00-bb722a339720.png)
こんな感じ
useEffectはロードしてるだけですが、今回はonSnapshotっていうfirestoreの関数を使ってリアルタイムで投稿を取得しています。これがなかったらリロードしないと最新の投稿を取得できません。
```
onSnapshot(collection(db, 'posts'), (posts) => {
    setPosts(
        posts.docs.map((post) => post.data()).sort((a,b) => b.created_at - a.created_at)
    )
})
```
`map`で投稿のデータを取ってきて`sort`でcreated_atの最新の投稿順で表示しています
あとは表示してるだけです。

### 全体まとめ
<details><summary>App.js</summary><div>

```javascript:App.js
import './App.css';
import NewPost from './NewPost';
import AllPosts from './AllPosts';

function App() {
    return (
        <div className="App">
            <NewPost />
            <AllPosts />
        </div>
    );
}

export default App;
```
</div></details>
<details><summary>NewPost.js</summary><div>

```javascript:NewPost.js
import { useState } from "react";
import { collection, addDoc } from "firebase/firestore";
import db from "./firebase";

const NewPost = () => {
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    const onSubmit = async (e) => {
        e.preventDefault();
        try {
            await addDoc(collection(db, "posts"), {
                title:title,
                content:content,
                created_at:new Date().getTime()
            });
            setTitle('')
            setContent('')
        } catch (error) {
            console.log(error);
        }
    };
    return (
        <>
            <p>新規投稿</p>
            <form onSubmit={onSubmit}>
                <input
                type="text"
                value={title}
                placeholder='タイトル'
                onChange={(e) => setTitle(e.target.value)}
                />
                <input
                type="text"
                value={content}
                placeholder='内容'
                onChange={(e) => setContent(e.target.value)}
                />
                <button type='submit'>投稿</button>
            </form>
        </>
    )
}

export default NewPost
```
</div></details>
<details><summary>AllPosts.js</summary><div>

```javascript:AllPosts.js
import { collection, onSnapshot } from 'firebase/firestore'
import React, { useEffect, useState } from 'react'
import db from './firebase'

const getStrTime = (time) => {
    let t = new Date(time);
    return (`${t.getFullYear()}/${t.getMonth()+1}/${t.getDate()} ${t.getHours()}:${t.getMinutes()}:${t.getSeconds()}`);
}

const AllPosts = () => {
    const [posts, setPosts] = useState([]);

    useEffect(() => {
        onSnapshot(collection(db, 'posts'), (posts) => {
            setPosts(
                posts.docs.map((post) => post.data()).sort((a,b) => b.created_at - a.created_at)
            )
        })
    }, []);

    return (
        <>
            <p>投稿一覧</p>
            {posts.map((post) => (
                <div className='post'>
                    <div className='title'>タイトル：{post.title}</div>
                    <div className='content'>内容：{post.content}</div>
                    <div className='created_at'>投稿日：{getStrTime(post.created_at)}</div>
                </div>
            ))}
        </>
    );
}

export default AllPosts
```
</div></details>