React + Firebase V9 (CRUDsystem)　この記事ですべて一挙公開！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654025/f36c503f-8593-e2fd-e1ef-c8fffa708b03.png)

https://firebase.google.com/docs/firestore?hl=ja

#目標:military_medal:
**Firebase　Vertion9**を使っています:bangbang:
今回は、Firebaseの**Firestoreサービス**を使って下記のようなCrudシステムを実装しています:bangbang:
ソースコードもすべて一挙公開します:blush:
参考にしていただけると幸いです:star:
解説は省きますが、ご質問があればメッセージください:envelope_with_arrow:
コピペするだけでも実装できます

![2022-01-23_00h17_49.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654025/e1e562b0-6618-a2ea-deea-79a525fd76aa.gif)

#使う技術:bulb:
###フロントエンド:computer:
https://ja.reactjs.org/

###バックエンド:closed_lock_with_key:
https://firebase.google.com/?hl=ja

###UI:ribbon:

https://mui.com/

https://www.framer.com/motion/


#必要なパッケージ:gift:
**create react app**
:arrow_right:npx create-react-app my-app

**firebase**
:arrow_right:npm install firebase

**Material UI & icons**
:arrow_right:npm install @mui/material @emotion/react @emotion/styled
:arrow_right:npm install @mui/icons-material

**Framer motion**
:arrow_right:npm install framer-motion

#ファイル構造
下記の様なファイル構造になっています:bangbang:
不要なファイルを削除したあと、”index.css”のソース一旦すべて削除してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654025/a6f0e51b-baf7-9ab6-d45f-06258693b3a1.png)

#firebase.js

```javascript:firebase.js
import { initializeApp } from "firebase/app";
// getFirestoreをインポートします
import { getFirestore } from "firebase/firestore"

const firebaseConfig = {
  // ここにconfig のデータをコピーペーストしてください。
  apiKey: " ",
  authDomain: " ",
  projectId: " ",
  storageBucket: " ",
  messagingSenderId: " ",
  appId: " "
};

const app = initializeApp(firebaseConfig);
//　dbにgetFirestore(app)を代入します
export const db = getFirestore(app)
```

#App.js

```javascript:App.js
import {
  addDoc,
  collection,
  deleteDoc,
  doc,
  onSnapshot,
  orderBy,
  query,
  serverTimestamp,
  updateDoc
} from 'firebase/firestore';
import { useEffect, useState } from 'react';
import { db } from './firebase';
import { Button, Stack, TextField } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import PublishIcon from '@mui/icons-material/Publish';
import CancelIcon from '@mui/icons-material/Cancel';
import { motion } from 'framer-motion';



const App = () => {

  const [documents, setDocuments] = useState([])
  const [title, setTitle] = useState("")
  const [body, setBody] = useState("")
  const [edit, setEdit] = useState(false)
  const [tempId, setTempId] = useState("")

  // Create
  const handleCreate = async (e) => {
    e.preventDefault();

    if (!title || !body) {
      return (
        alert("Please put the all input")
      )
    }

    const collectionRef = collection(db, "blogs");
    await addDoc(collectionRef, {
      title: title,
      body: body,
      timestamp: serverTimestamp(),
    })

    setTitle("");
    setBody("");
  }

  // Read
  useEffect(() => {
    const collectionRef = collection(db, "blogs");
    const q = query(collectionRef, orderBy("timestamp", "desc"));

    onSnapshot(q, (querySnapshot) => {
      const values = [];
      querySnapshot.forEach((snapshot) => {
        values.push({
          ...snapshot.data(),
          id: snapshot.id,
        })
      })
      setDocuments(values)
    })
  }, [])

  // Update
  const handleUpdate = async (id, postTitle, postBody) => {
    setEdit(true);
    setTitle(postTitle)
    setBody(postBody)
    setTempId(id);
  }

  const handleSubmit = async (e) => {
    e.preventDefault();
    const collectionRef = collection(db, "blogs");
    const ref = doc(collectionRef, tempId);
    const payload = {
      title: title,
      body: body,
      timestamp: serverTimestamp(),
    }

    if (!title || !body) {
      alert("please input something")
      return
    }
    await updateDoc(ref, payload)
    setTitle("");
    setBody("");
    setEdit(false)
  }

  // Delete
  const handleDelete = async (id) => {
    const collectionRef = collection(db, "blogs");
    const ref = doc(collectionRef, id);
    if (window.confirm("Are you sure for delete？")) {
      await deleteDoc(ref);

    } else {
      alert("cancel")
    }
  }

  return (
    <div className='main'>

      <div className="create">
        <h1>Create</h1>
        <TextField
          id="standard-basic"
          label="Title"
          variant="standard"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        <p></p>
        <TextField
          id="outlined-multiline-static"
          label="Body"
          multiline
          rows={5}
          value={body}
          onChange={(e) => setBody(e.target.value)}
        />
        <p></p>

        {!edit ?
          (<Button
            variant="contained"
            onClick={handleCreate}>
            Submit
          </Button>
          ) : (
            <Stack direction="column" spacing={2}>
              <Button
                variant="contained"
                startIcon={<PublishIcon style={{ color: "white" }} />}
                onClick={handleSubmit}>Update
              </Button>
              <Button
                variant="contained"
                color="error"
                startIcon={<CancelIcon style={{ color: "white" }} />}
                onClick={() => {
                  setTitle("")
                  setBody("")
                  setEdit(false)
                }
                }>Cancel</Button>
            </Stack>
          )
        }
      </div>

      <div >

        {documents.length ?
          (<div className='list'>
            {documents.map((blog) => (
              <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                transition={{ delay: 0.1 }}
                className='card' key={blog.id}>
                <h4>Title :<span>{blog.title}</span></h4>
                <p style={{
                  border: "1px solid black",
                  borderRadius: "5px"
                }}><span style={{
                  whiteSpace: 'pre-line',

                }}>{blog.body}</span></p>

                {blog.timestamp &&

                  (blog.timestamp.toDate().getFullYear() + "-" +
                    ("0" + (blog.timestamp.toDate().getMonth() + 1)).slice(-2) + "-" +
                    ("0" + blog.timestamp.toDate().getDate()).slice(-2) + " " +
                    ("0" + blog.timestamp.toDate().getHours()).slice(-2) + ":" +
                    ("0" + blog.timestamp.toDate().getMinutes()).slice(-2))


                }



                {!edit &&
                  <Stack direction="row" spacing={2}>
                    <Button
                      variant="outlined"
                      color='success'
                      startIcon={<PublishIcon style={{ color: "green" }} />}
                      onClick={() => handleUpdate(blog.id, blog.title, blog.body)}>Update
                    </Button>
                    <Button
                      variant="outlined"
                      color="error"
                      startIcon={<DeleteIcon />}
                      onClick={() => handleDelete(blog.id)}>Delete
                    </Button>
                  </Stack>
                }
              </motion.div>
            ))}
          </div>
          ) : (
            <div>nothing data</div>
          )
        }
      </div>
    </div >
  );
};


export default App;
```

#index.css

```css:index.css
body {
  background-color: #dfe0f1;
}

.main {
  margin: auto;
  text-align: center;
  display: flex;
  justify-content: space-evenly;
}

.create {
  top: 0;
}

.list {
  width: 500px;
  display: grid;
  grid-template-columns: repeat(1, 1fr);
}

.card {
  background-color: azure;
  margin: 15px;
  padding: 15px;
  width: auto;
  box-shadow: 19px 17px 43px -5px #c7c8e6;
  border-radius: 5px;
  border: 1px solid rgb(194, 183, 183);
}

.card:hover {
  cursor: pointer;
}
```

#参考

https://youtu.be/QaYts9sPmcY

https://youtu.be/vUe91uOx7R0

https://youtu.be/jCY6DH8F4oc

https://youtube.com/playlist?list=PLqFvlDFoiZ-2SAX7YXCYtb28K4IooCIlS

#次回
次回は、Firebase Authenticationを公開します 

https://firebase.google.com/docs/auth?hl=ja