目標

このハンズオンでは、Firebase Authentication、Cloud Firestore、Reactを利用したTodoウェブアプリ」を作成します。

(完成画像)

学習内容

・ ウェブアプリから Cloud Firestore へのデータの読み取りと書き込み

・ Cloud Firestore データの変更をリアルタイムで行う

・ Firebase Authentication とセキュリティールールを使用して、Cloud Firestoreデータを保護する

・ 複雑な Cloud Firestore クエリを作成する。

クエリとは

必要なもの

このハンズオンを開始する前に、以下のものが用意されていることを確認してください

・ Googleアカウント

Node.js をインストールします。

Node.js とは、JavaScript をサーバーサイドで実行するためのランタイムです。React や Firebase などの開発で必要になるため、インストールしておきましょう。

ランタイムとは

Node.js の公式サイトから、自分の環境に合わせたインストーラーをダウンロードして実行します。

https://nodejs.org/ja/

LTSの推奨版 をダウンロードしてください。ダウンロードが完了したら、インストーラーをダブルクリックしてインストールを開始します。

インストールが完了したら、コマンドプロンプトを開き、以下のコマンドを入力してバージョンを確認してみましょう。

node -v

バージョン番号が表示されれば、Node.js のインストールは成功です。

VSCode は、Visual Studio Code の略称で、Microsoft が開発したオープンソースのコードエディタです。JavaScript や TypeScript などの言語に対応し、多彩な機能を持っています。以下の手順で VSCode をインストールしましょう。

1.VSCode のダウンロードページにアクセスします。

https://code.visualstudio.com/

2.ページ内の「Download for Windows」ボタンをクリックします。

3.ダウンロードが完了したら、ダウンロードしたファイルを開きます。

4.同意する場合は、「同意します。」にチェックを入れて、「次へ」をクリックします。

5.インストール先のフォルダーを指定し、「次へ」をクリックします。

6.スタートメニューにショートカットを作成するかどうかを選択し、「次へ」をクリックします。

7.インストールが開始されます。完了まで待ちます。

8.「完了」をクリックして、インストールが完了したことを確認します。

これで、VSCode がインストールされました。

ここでは、インストールしておくべき拡張機能を紹介します。

左側の四角形が4つ集まったアイコンを探してください。

拡張機能の検索機能が一番上にでてくるので、次の拡張機能名をコピペしてインストールして下さい。

・Japanese Language Pack for Visual Studio Code

・Prettier - Code formatter

・Firebase

・ES7 React/Redux/GraphQL/React-Native snippets

1. VScodeを開き、「ctrl + o」を押し、任意の場所にフォルダを作ります。

2. 作成したフォルダを開きます。

これで、プロジェクトの作成準備が出来ました。

Reactプロジェクトの作成

前章で作成した、Reactプロジェクトを作成するためにnpx create-react-appコマンドを使用します。

ターミナルで以下のコマンドを実行してください。

1. VScode上で「ctrl + @」を押し、VScode上のターミナルを開きます。

2.ターミナル上で以下のコマンドを実行します。

npx create-react-app フォルダ名

(例) npx create-react-app my-app

上記のコマンドでmy-appという名前のプロジェクトが作成されます。この プロジェクト名は任意の名前 に変更できます。

.(ピリオド)を付けた場合のコマンドは以下のようになります

npx create-react-app .

これで、VScodeでフォルダを作成した場所に、Reactプロジェクトが作成されます。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Firebase SDK・CLIの導入

  1. VScode上のコンソールを開きます。「ctrl+shift+@」でショートカットです。
  2. npm install firebase を実行し、SDKを導入します。
npm install firebase
  1. npm install -g firebase-toolsを実行し、CLIを導入します。
npm install -g firebase-tools
  1. firebase --versionを実行して、バージョンが表示されるか確認しましょう。
firebase --version
  1. firebase loginを実行して、コンソールの指示に従い、firebaseプロジェクトを作成したGmailアドレスを入力します。
firebase login
  1. firebase initを実行して、コンソールの指示に従い作成したfirebaseプロジェクトを選択します。
firebase init

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Reactローカルサーバの立ち上げ

次のコマンドを実行することで、Webブラウザにローカルサーバを立ち上げます。

cd my-app

.(ピリオド)」で実行した方、cdで移動した方は以下のコマンドを実行して下さい。

npm start

これで、ローカルサーバを立ち上げることが出来ました。

今後はしばらくこのローカルサーバを使って開発を続けていきます。

Firebase プロジェクトを作成する

  1. Firebase コンソールで、「プロジェクトを追加」をクリックして、Firebaseプロジェクトに、任意の名前をつけます。

  1. プロジェクトを作成」をクリックします。

これから構築するアプリケーションは、ウェブで利用できるいくつかの Firebase サービスを使用します。

・ユーザーを簡単に識別するためのFirebase Authentication

・構造化データをクラウドに保存し、データが更新されたときに即座に通知を受け取るCloud Firestore

・静的アセットをホストして提供するためのFirebase Hosting

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

App.jsの作成

App.jsの中身を全て削除する。

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App

rccpと記述する

rccp

余分なものを削除して、必要なものを追加します。

これが・・・

import React, { Component } from 'react'
//削除↓↓↓
// import PropTypes from 'prop-types'

export class App extends Component {
  //削除↓↓↓
  // static propTypes = {
  //   prop: PropTypes
  // }

  render() {
    return (
      // 追加↓↓↓
      <div className='wrapper'>
        //追加↓↓↓
        <Header/>
      </div>
    )
  }
}
//追加↓↓↓
export default App

こうじゃ!

import React, { Component } from 'react'

export class App extends Component {
  render() {
    return (
      <div className='wrapper'>
        <Header/>
      </div>
    )
  }
}
export default App

すっきりしました。

次にCSSを作成する時に、className='wrapper'にも適用するのでCSSをimportします。

import React, { Component } from 'react'
import { Header } from './Components/Header'    //自動追加
import './index.css'    //追加

export class App extends Component {
  render() {
    return (
      <div className='wrapper'>
        <Header/>
      </div>
    )
  }
}
export default App

後ほどCSSを作成する時に適用されます。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

ComponentsフォルダとHeader.jsの作成

  1. srcフォルダにComponentsフォルダを新規作成
  2. ComponentsフォルダにHeader.jsを作成
  3. Header.jsrafcと記述
rafc

そうすると、このようなものができる。

import React from 'react'

export const Header = () => {
    return (
        <div>Header</div>
    )
}

CSSをHeaderに適用するために、classNameを付ける。

そして、CSSをimportしましょう。

import React from 'react'
import '../index.css'   //追加

export const Header = () => {
    return (
        <div className='header-box'>    //追加
            Header
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

index.cssの作成

  1. srcフォルダの中にあるindex.cssを探す。
  2. 次のように記述する。
*{
  margin: 0;
  padding: 0;
}

div.wrapper{
  overflow-x: hidden;
  overflow-y: auto;
}

/* header */
.header-box{
  width: 100%;
  height: auto;
  padding: 50px;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  background-color: #0170ad;
}

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Header.jsの作成

Header.jsに新しいCSSを追加するためのclassNameを追加しましょう。

import React from 'react'
import '../index.css'

export const Header = () => {
    return (
        <div className='header-box'>
            //追加↓↓↓
            <div className='leftside'>
                <div className='img'></div>
                <div className='content'>
                    <div className='heading-big'></div>
                    <div className='heading-small'></div>
                </div>
            </div>
            <div className='rightside'></div>
            //追加↑↑↑
        </div>
    )
}

次に、Headerに説明文を追加して、imgタグを追加しましょう。

その前に、Todoのロゴ?アイコン?となるような画像を作成するか、フリー画像などをダウンロードしましょう。

  1. srcフォルダの中にimagesフォルダを作成
  2. その中に作成orダウンロードしたファイルを入れる。
  3. その画像をimportして表示します。
import React from 'react'
//追加↓↓↓
import todoIcon from '../images/TodoAppicon.png'
import '../index.css'

export const Header = () => {
    return (
        <div className='header-box'>
            <div className='leftside'>
                <div className='img'>
                    //追加↓↓↓
                    <img src={todoIcon} alt='todoIcon'/>
                </div>
                <div className='content'>
                    <div className='heading-big'>
                        //追加↓↓↓
                        work to do?
                    </div>
                    <div className='heading-small'>
                        //追加↓↓↓
                        Lets make a list!
                    </div>
                </div>
            </div>
            <div className='rightside'>
            
            </div>
        </div>
    )
}

追加したclassNameにCSSを適用するためにCSSの章に行きましょう。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

index.cssの作成

それでは、追加したclassNameにCSSを適用させるために以下のコードを追加しましょう。

* {
  margin: 0;
  padding: 0;
}

div.wrapper {
  overflow-x: hidden;
  overflow-y: auto;
}

/* header */
.header-box {
  width: 100%;
  height: auto;
  padding: 50px;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  background-color: #0170ad;
}

//追加↓↓↓
.header-box .leftside {
  flex: 1;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.header-box .leftside .img {
  width: 170px;
  height: 170px;
}

.header-box .leftside .img img {
  width: 100%;
  height: 100%;
}

.header-box .leftside .content {
  color: #fff;
}

.header-box .leftside .content .heading-big {
  font-size: 42px;
}

.header-box .leftside .content .heading-small {
  font-size: 24px;
}

.header-box .rightside {
  
}
//追加↑↑↑

これでロゴ?アイコン?と説明の大小の設定が出来ました。

レスポンシブにきれいな状態にはなっていませんが、ブラウザを最大化してみるとある程度整ったものになると思います。

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Header.jsの作成

ここでは、react-router-domimportし、設定をしていきます。

  1. VScodeのターミナル上で「ctrl+shift+@」を押して、新しいターミナルを開きます。
  2. 次のコマンドnpm i react-router-domを実行して、Webページ内のリンクを作るためのパッケージをインストールします。
npm i react-router-dom
  1. さらに次のコマンドnpm i bootstrapを実行して、フレームワークをインストールします。
npm i bootstrap
  1. インストールが終わったら、Header.jsApp.jsにそれぞれimportします。
import React from 'react'
import todoIcon from '../images/TodoAppicon.png'
//追加↓↓↓
import { Link } from 'react-router-dom'
//追加↓↓↓
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'

export const Header = () => {
    return (
        <div className='header-box'>
            <div className='leftside'>
                <div className='img'>
                    <img src={todoIcon} alt='todoIcon' />
                </div>
                <div className='content'>
                    <div className='heading-big'>
                        work to do?
                    </div>
                    <div className='heading-small'>
                        Lets make a list!
                    </div>

                </div>
            </div>
            <div className='rightside'>
                
            </div>
        </div>
    )
}
  1. 次にclassName='rightside'にサインアップとログイン機能をつけます。
import React from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'

export const Header = () => {
    return (
        <div className='header-box'>
            <div className='leftside'>
                <div className='img'>
                    <img src={todoIcon} alt='todoIcon' />
                </div>
                <div className='content'>
                    <div className='heading-big'>
                        work to do?
                    </div>
                    <div className='heading-small'>
                        Lets make a list!
                    </div>

                </div>
            </div>
            <div className='rightside'>
                //追加↓↓↓
                <Link to="signup" className='btn btn-primary btn-md'>
                    SIGN UP
                </Link>
                <Link to="login" className='btn btn-secondary btn-md'>
                    LOGIN
                </Link>
                //追加↑↑↑
            </div>
        </div>
    )
}
  1. 最後に年月日を表示するための雛形を作ります。
import React from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'

export const Header = () => {
    return (
        <div className='header-box'>
            <div className='leftside'>
                <div className='img'>
                    <img src={todoIcon} alt='todoIcon' />
                </div>
                <div className='content'>
                    <div className='heading-big'>
                        work to do?
                    </div>
                    <div className='heading-small'>
                        Lets make a list!
                    </div>

                </div>
            </div>
            <div className='rightside'>
                <Link to="signup" className='btn btn-primary btn-md'>
                    SIGN UP
                </Link>
                <Link to="login" className='btn btn-secondary btn-md'>
                    LOGIN
                </Link>
                //追加↓↓↓
                <br/>
                <div className='date-section'>
                    <span>date</span>
                    <span>Month</span>
                    <span>year</span>
                    <span>day</span>
                </div>
                //追加↑↑↑
            </div>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Home.jsの作成

次にHome.jsを新たにComponentsフォルダの中に作成します。

  1. srcフォルダの中のComponentsフォルダに行く。
  2. 新規ファイルを作成し、Home.jsと入力します。
import React from 'react'
import '../index.css'
import { Header } from './Header'

export const Home = () => {
    return (
        <div className='wrapper'>
            <Header />
        </div>
    )
}

以上のように、元々Header.jsに記述してあったものを移動させました。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

App.jsの作成

以下のコードをApp.jsに追加しましょう。

import React, { Component } from 'react'
import './index.css'
//追加↓↓↓
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'

export class App extends Component {
  render() {
    return (
      //追加↓↓↓
      <Router>
        <Routes>
         <Route path='/' element={<Home/>} />
        </Routes>
      </Router>
      //追加↑↑↑
    )
  }
}
export default App

これで、内部Webのルーティングの準備が整いました。現状では、サイトを開いたときにHomeディレクトリにアクセスするようになっています。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

index.cssの作成

SIGN UPやLOGINと年月日にCSSを適用してみましょう。

* {
  margin: 0;
  padding: 0;
}

div.wrapper {
  overflow-x: hidden;
  overflow-y: auto;
}

/* header */
.header-box {
  width: 100%;
  height: auto;
  padding: 50px;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  background-color: #0170ad;
}

.header-box .leftside {
  flex: 1;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.header-box .leftside .img {
  width: 170px;
  height: 170px;
}

.header-box .leftside .img img {
  width: 100%;
  height: 100%;
}

.header-box .leftside .content {
  color: #fff;
}

.header-box .leftside .content .heading-big {
  font-size: 42px;
}

.header-box .leftside .content .heading-small {
  font-size: 24px;
}

//追加↓↓↓
.header-box .rightside {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.header-box .rightside .btn, .header-box .rightside .btn:hover{
  width: 100px;
  margin-bottom: 5px;
  color: #fff;
  text-decoration: none;
}

.date-section{
  color: #fff;
}

.date-section span{
  margin-left: 4px;
}
//追加↑↑↑

SIGN UP/LOGINボタンの長さが均等になり、年月日の間に隙間ができましたか?

Reactコンポーネントの作成

この章では、SIGN UPとLOGINのページを作るためにjsファイルを作成して、react-router-domでサイト内ページをそれぞれ移動出来るようにしましょう。

まずは、Signup.jsLogin.jsファイルを作成しましょう。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Signup.jsの作成

  1. srcフォルダの中のComponentsフォルダに行く。
  2. 新規ファイルを作成し、Signup.jsと入力します。

rafcと記述して、

rafc

以下のようになるか確認してください。

import React from 'react'

export const Signup = () => {
    return (
        <div>Signup</div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Login.jsの作成

  1. srcフォルダの中のComponentsフォルダに行く。
  2. 新規ファイルを作成し、Login.jsと入力します。

rafcと記述して、

rafc

以下のようになるか確認してください。

import React from 'react'

export const Login = () => {
    return (
        <div>Login</div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

App.jsの作成

それぞれのルートを設定追加します。

import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
//追加↓↓↓
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
//追加↑↑↑

export class App extends Component {
  render() {
    return (
      <Router>
        <Routes>
          <Route path='/' element={<Home/>} />
          //追加↓↓↓
          <Route path='/signup' element={<Signup />} />
          <Route path='/login' element={<Login />} />
          //追加↑↑↑
        </Routes>
      </Router>
    )
  }
}
export default App

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Header.jsの作成

ここではState(ステート)Effect(エフェクト)を使用して、年月日を取得する機能を追加します。

//useEffect,useStateを追加↓↓↓(自動追加されるが一応確認)
import React, { useEffect, useState } from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'

export const Header = () => {

    //追加↓↓↓
    const [year, setYear] = useState(null);
    const [month, setMonth] = useState(null);
    const [date, setDate] = useState(null); 
    const [day, setDay] = useState(null);

    useEffect(() => {
        const currentDate = new Date();
        const currentYear = currentDate.getFullYear();
        const currentDateOfMonth = currentDate.getDate();
        const currentMonth = currentDate.toLocaleString('ja-JP', { month: 'long' });
        const currentDay = currentDate.toLocaleDateString('ja-JP', { weekday: 'long' });

        setYear(currentYear);
        setDate(currentDateOfMonth);
        setMonth(currentMonth);
        setDay(currentDay);
    }, [])
    //追加↑↑↑

    return (
        <div className='header-box'>
            <div className='leftside'>
                <div className='img'>
                    <img src={todoIcon} alt='todoIcon' />
                </div>
                <div className='content'>
                    <div className='heading-big'>
                        work to do?
                    </div>
                    <div className='heading-small'>
                        Lets make a list!
                    </div>

                </div>
            </div>
            <div className='rightside'>
                <Link to="signup" className='btn btn-primary btn-md'>
                    SIGN UP
                </Link>
                <Link to="login" className='btn btn-secondary btn-md'>
                    LOGIN
                </Link>
                <br />
                <div className='date-section'>
                    //変更↓↓↓
                    <span>{year}年</span>
                    <span>{month}</span>
                    <span>{date}日</span>
                    <span>{day}</span>
                    //変更↑↑↑
                </div>
            </div>
        </div>
    )
}

useStateはReactの状態管理機能の一つであり、関数コンポーネント内で変数を宣言するように値を管理することができます。

useEffectは、Reactのライフサイクルメソッドの一つであり、コンポーネントが`マウント`された後、更新された後、アンマウントされる前に実行される関数を指定することができます。

コンポーネントがマウントされた後、ユーザーがコンポーネントを操作したり、プロパティが更新されたりすると、Reactは自動的に再描画を行います。このときは「更新」というフェーズに入ります。

Reactコンポーネントの作成

この章では、Signup.jsLogin.jsを作成していきます。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Signup.jsの作成

User NameとEmailとPasswordを入力してもらい、登録する部分を作成します。

import React from 'react'
import { Link } from 'react-router-dom'

export const Signup = () => {
    return (
    //追加↓↓↓
        <div className='container'>
            <br/>
            <br/>
            <h2>SIGN UP NOW!</h2>
            <br/>
            <form autoComplete="off" className='form-group'>
                <label>User Name</label>
                <input type='text' className='form-control' required/>
                <br/>
                <label>Email</label>
                <input type='email' className='form-control' required/>
                <br/>
                <label>Password</label>
                <input type='password' className='form-control' required/>
                <br/>
                <button type='submit' className='btn btn-success mybtn2'>
                    REGISTER
                </button>
            </form>

            <span>Already have an account? Login
                <Link to='login'>here</Link>
            </span>
        </div>
    //追加↑↑↑
    )
}

以前、bootstrapを導入したおかげできれいな画面が出来上がりました。

そして、入力した状態を保存するために、useStatusを導入します。

import React,{useState} from 'react'
import { Link } from 'react-router-dom'

export const Signup = () => {

  //追加↓↓↓
    const[userName,setUserName]=useState('');
    const[email,setEmail]=useState('');
    const[password,setPassword]=useState('');

    const[registerError,setRegisterError]=useState('');

    const handleRegister=(e)=>{
        e.preventDefault();
        console.log(userName,email,password);
    }
  //追加↑↑↑

    return (
        <div className='container'>
            <br/>
            <br/>
            <h2>SIGN UP NOW!</h2>
            <br/>
            <form autoComplete="off" className='form-group'
            onSubmit={handleRegister}>
                <label>User Name</label>
                <input type='text' className='form-control' 
                    required onChange={(e)=>setUserName(e.target.value)}
                    value={userName}
                />
                <br/>
                <label>Email</label>
                //追加・変更↓↓↓
                <input type='email' className='form-control'
                    required onChange={(e)=>setEmail(e.target.value)}
                    value={email}
                />
                //追加・変更↑↑↑
                <br/>
                <label>Password</label>
                //追加・変更↓↓↓
                <input type='password' className='form-control'
                    required onChange={(e)=>setPassword(e.target.value)}
                    value={password}
                />
                //追加・変更↑↑↑
                <br/>
                <button type='submit' className='btn btn-success register'>
                    REGISTER
                </button>
            </form>
      //追加↓↓↓
            {registerError && <div className='error-msg'>
                {registerError}
            </div>}
      //追加↑↑↑
            <span>Already have an account? Login
                <Link to='login'>here</Link>
            </span>
        </div>
    )
}

コードの上の方にあるconst[,]=useStatus();では、ここで使用するuserName,email,passwordを保存するための場所を初期化をしています。

その下は、同じくエラー文を送るための場所を初期化しています。

handleRegisterのアロー関数が付いている場所では、フォームが送信された時の文が記述されており、この章では一度console.logに出力して確認しています。

またJSX、return文の中にあるinput文を改変し、コードの上で宣言したStatusにそれぞれ情報を入力するようにしています。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

index.cssの作成

REGISTERをレスポンシブに対応させて、スマートフォンで使いやすくなるように変更しましょう。

* {
  margin: 0;
  padding: 0;
}

div.wrapper {
  overflow-x: hidden;
  overflow-y: auto;
}

/* header */
.header-box {
  width: 100%;
  height: auto;
  padding: 50px;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  background-color: #0170ad;
}

.header-box .leftside {
  flex: 1;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.header-box .leftside .img {
  width: 170px;
  height: 170px;
}

.header-box .leftside .img img {
  width: 100%;
  height: 100%;
}

.header-box .leftside .content {
  color: #fff;
}

.header-box .leftside .content .heading-big {
  font-size: 42px;
}

.header-box .leftside .content .heading-small {
  font-size: 24px;
}

.header-box .rightside {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.header-box .rightside .btn, .header-box .rightside .btn:hover{
  width: 100px;
  margin-bottom: 5px;
  color: #fff;
  text-decoration: none;
}

.date-section{
  color: #fff;
}

.date-section span{
  margin-left: 4px;
}

//追加↓↓↓
/* sign up */
@media(max-width:539px){
  .register{
    width:100%
  }
}
//追加↑↑↑

この章では、ついにFirebaseと連携します。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

firebase.confiig.jsの作成

  1. srcフォルダの中にservicesフォルダを作成し、行く。
  2. 新規ファイルを作成し、firebase.config.jsと入力します。
  3. 自身が作成したFirebaseプロジェクトに行く。
  4. プロジェクトの設定(歯車マーク)の項目に行く。
  5. SDK の設定と構成Configを選択して中身をコピーして下さい。
  6. そして次のように記述し、firebaseConfigの部分はコピペと置き換えて下さい
import { initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getFirestore } from 'firebase/firestore'

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

const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
const db = getFirestore(app)

export { auth, db }

これでfirebaseを利用する準備が整いました

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Signup.jsの作成

先程、cosole.logで出力していた情報を今度は、Firebase Firestoreのデータベースに格納します。また強制的にsignupした際に移動するためにuseNavigateを新たにimportします。

import React, { useState } from 'react'
//追加↓↓↓
import { Link,useNavigate } from 'react-router-dom'
//追加↓↓↓
import { createUserWithEmailAndPassword } from 'firebase/auth'
import { doc, setDoc } from "firebase/firestore";
import { auth, db } from '../services/firebase.config'
//追加↑↑↑

export const Signup = (props) => {

    const [userName, setUserName] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');

    const [registerError, setRegisterError] = useState('');

  //変更・追加↓↓↓
    const navigate = useNavigate();
    const handleRegister = async (e) => {
        e.preventDefault();
        try {
            const userCredential = await createUserWithEmailAndPassword(auth, email, password);
            const { uid } = userCredential.user;
            await setDoc(doc(db, "users", uid), {
                userName: userName,
                Email: email,
                Password: password
            });
            setUserName("");
            setEmail("");
            setPassword("");
            setRegisterError("");
            navigate('/login');
        } catch (error) {
            setRegisterError(error.message);
        }
    };
  //変更・追加↑↑↑


    return (
        <div className='container'>
            <br />
            <br />
            <h2>SIGN UP NOW!</h2>
            <br />
            <form autoComplete="off" className='form-group'
                onSubmit={handleRegister}>
                <label>User Name</label>
                <input type='text' className='form-control'
                    required onChange={(e) => setUserName(e.target.value)}
                    value={userName}
                />
                <br />
                <label>Email</label>
                <input type='email' className='form-control'
                    required onChange={(e) => setEmail(e.target.value)}
                    value={email}
                />
                <br />
                <label>Password</label>
                <input type='password' className='form-control'
                    required onChange={(e) => setPassword(e.target.value)}
                    value={password}
                />
                <br />
                <button type='submit' className='btn btn-success register'>
                    REGISTER
                </button>
            </form>
            {registerError && <div className='error-msg'>
                {registerError}
            </div>}

            <span>Already have an account? Login
                <Link to='/login'>here</Link>
            </span>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Login.jsの作成

まずは、入力フォームの雛形を作ります。

import React from 'react'
//追加↓↓↓
import { Link } from 'react-router-dom'

export const Login = () => {
    return (
        //変更・追加↓↓↓
        <div className='container'>
            <br />
            <br />
            <h2>LOGIN HERE</h2>
            <br />
            <form autoComplete="off" className='form-group'>
                <label>Enter Email</label>
                <input type="email" className='form-control'required/>
                <br />
                <label>Enter Password</label>
                <input type="password" className='form-control'required/>
                <br />
                <button type="submit" className='btn btn-success mybtn2'>
                    LOGIN
                </button>
            </form>

            <span>Dont have an account? Create One
                <Link to="/signup"> here</Link></span>

        </div>
        //変更・追加↑↑↑
    )
}

次に、useStatusとFirestoreに書き込むコードを導入してログイン機能を実装します。

Signup.jsでも追加したように、useNavigateimportして移動するようにします。

import React, { useState } from 'react'
//追加↓↓↓
import { Link,useNavigate } from 'react-router-dom'
//追加↓↓↓
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../services/firebase.config'
//追加↑↑↑

export const Login = () => {

  //追加↓↓↓
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');

    const [loginError, setLoginError] = useState('');

  const navigate = useNavigate();

    const handleLogin = async (e) => {
        e.preventDefault();
        try {
            await signInWithEmailAndPassword(auth, email, password);
            setEmail('');
            setPassword('');
            setLoginError('');
            navigate('/');
        } catch (error) {
            setLoginError(error.message);
        }
    };
  //追加↑↑↑

    return (
        <div className='container'>
            <br />
            <br />
            <h2>LOGIN HERE</h2>
            <br />
            <form autoComplete="off" className='form-group'
                onSubmit={handleLogin}>

                <label>Enter Email</label>
                <input type="email" className='form-control'
                  //追加↓↓↓
                    required onChange={(e) => setEmail(e.target.value)}
                    value={email}
                  //追加↑↑↑
                />
                <br />
                <label>Enter Password</label>
                <input type="password" className='form-control'
                  //追加↓↓↓
                    required onChange={(e) => setPassword(e.target.value)}
                    value={password}
                  //追加↑↑↑
                />
                <br />
                <button type="submit" className='btn btn-success mybtn2'>
                    LOGIN
                </button>
            </form>

          //追加↓↓↓
            {loginError && <div className='error-msg'>
                {loginError}
            </div>}
          //追加↑↑↑

            <span>Dont have an account? Create One
                <Link to="/signup"> here</Link></span>

        </div>
    )
}

ここでは、ログイン情報をホーム画面に表示する機能を作っていきます。

また、登録されていないメールアドレスでログインした場合にはNot foundを返します。

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

NotFound.jsの作成

Not foundと表示するためのページを作ります。またこのページは次のApp.jsの作成でルートを設定します。

  1. Componentsフォルダに行きNotFound.jsファイルを作成します。
  2. rafcと入力して
rafc
  1. 次のようにして下さい。
import React from 'react'

export const NotFound = () => {
    return (
        <div>NotFound</div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

App.jsの作成

NotFound.jsのルートの設定と、Headerにログイン情報を渡すためにデータを取得します。

import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
import { NotFound } from './Components/NotFound'
import { auth, db } from './services/firebase.config'
//追加↓↓↓
import { doc, getDoc } from 'firebase/firestore';

export class App extends Component {

 //追加↓↓↓
  state = {
    currentUser: null
  }

  componentDidMount() {
    auth.onAuthStateChanged(user => {
      if (user) {
        getDoc(doc(db, 'users', user.uid)).then(snapshot => {
          this.setState({
            currentUser: snapshot.data().userName
          })
        })
      }
      else {
        console.log("user is not signed")
      }
    })
  }
 //追加↑↑↑

  render() {
    return (
      <Router>
        <Routes>
         //変更・追加↓↓↓
          <Route path='/' element={
            <Home currentUser={this.state.currentUser}
          />} />
         //変更・追加↑↑↑
          <Route path='/signup' element={<Signup />} />
          <Route path='/login' element={<Login />} />
         //追加↓↓↓
          <Route path='*' element={<NotFound />} />
        </Routes>
      </Router>
    )
  }
}

export default App

localhostのURLに/NotFoundを付けて移動するかどうか確認しておきましょう。

ここではサインアップまたはログイン後のHeader画面と、Todoアプリの画面を作成していきます。

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Home.jsの作成

まずは、Todoを追加する画面を作りましょう!

import React from 'react'
import '../index.css'
import { Header } from './Header'

export const Home = ({ currentUser }) => {

    

    return (
        <div className='wrapper'>
          //変更・追加↓↓↓
            <Header currentUser={currentUser} />
            <br />
            <br />
            <div className='container'>
                <form autoComplete='off' className='form-group'>

                    {currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required/>
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>

                    </>}

                    {!currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required disabled
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                disabled style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>
                        <div className='error-msg'>
                            Please register your account or login to use application
                        </div>
                    </>}
                </form>
            </div>
          //変更・追加↑↑↑
        </div>
        )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Header.jsの作成

ユーザー情報をHeaderに表示するために、firestoreからデータをとってきます。また、ログインしている状態を表示するのとログアウトボタンを生成します。

import React, { useEffect, useState } from 'react'
import todoIcon from '../images/TodoAppicon.png'
import { Link } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import '../index.css'
//追加↓↓↓
import { auth } from '../services/firebase.config'

export const Header = ({ currentUser }) => {

    const [year, setYear] = useState(null);
    const [date, setDate] = useState(null);
    const [month, setMonth] = useState(null);
    const [day, setDay] = useState(null);

    useEffect(() => {
        const currentDate = new Date();
        const currentYear = currentDate.getFullYear();
        const currentDateOfMonth = currentDate.getDate();
        const currentMonth = currentDate.toLocaleString('ja-JP', { month: 'long' });
        const currentDay = currentDate.toLocaleDateString('js-JP', { weekday: 'long' });

        setYear(currentYear);
        setDate(currentDateOfMonth);
        setMonth(currentMonth);
        setDay(currentDay);
    }, [])
  //追加↓↓↓
    const handleLogout = () => {
        auth.signOut().then(() => {
            window.location.reload();
        });
    }
  //追加↑↑↑


    return (
        <div className='header-box'>
            <div className='leftside'>
                <div className='img'>
                    <img src={todoIcon} alt='todoIcon' />
                </div>
                <div className='content'>
                    <div className='heading-big'>
                        work to do?
                    </div>
                    <div className='heading-small'>
                        Let's make a list!
                    </div>

                </div>
            </div>
            <div className='rightside'>
                //変更・追加↓↓↓
                {!currentUser && <>
                    <Link className='btn btn-primary btn-md' to="signup">
                        SIGN UP
                    </Link>


                    <Link className='btn btn-secondary btn-md' to="login">
                        LOGIN
                    </Link>

                    <br></br>
                    <div className='date-section'>
                        <span>{year}年</span>
                        <span>{month}</span>
                        <span>{date}日</span>
                        <span>{day}</span>
                    </div>

                </>}

                {currentUser && <div className='welcome-div'>

                    <h2>WELCOME</h2>
                    <h5>{currentUser}</h5>
                    <br></br>
                    <div className='date-section'>
                        <span>{year}年</span>
                        <span>{month}</span>
                        <span>{date}日</span>
                        <span>{day}</span>
                    </div>
                    <br></br>
                    <button className='btn btn-danger'
                        onClick={handleLogout}>LOGOUT</button>
                </div>}
                //変更・追加↑↑↑
            </div>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

index.cssの作成

Home.jserror-msgHeader.jswelcom-divにcssを適用します。

* {
  margin: 0;
  padding: 0;
}

div.wrapper {
  overflow-x: hidden;
  overflow-y: auto;
}

/* header */
.header-box {
  width: 100%;
  height: auto;
  padding: 50px;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  background-color: #0170ad;
}

.header-box .leftside {
  flex: 1;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.header-box .leftside .img {
  width: 170px;
  height: 170px;
}

.header-box .leftside .img img {
  width: 100%;
  height: 100%;
}

.header-box .leftside .content {
  color: #fff;
}

.header-box .leftside .content .heading-big {
  font-size: 42px;
}

.header-box .leftside .content .heading-small {
  font-size: 24px;
}

.header-box .rightside {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.header-box .rightside .btn, .header-box .rightside .btn:hover{
  width: 100px;
  margin-bottom: 5px;
  color: #fff;
  text-decoration: none;
}

.date-section{
  color: #fff;
}

.date-section span{
  margin-left: 4px;
}

/* sign up */
@media(max-width:539px){
  .register{
    width:100%
  }
}

//追加↓↓↓
.error-msg{
  color: red;
  width:100%;
  font-size:14px;
  font-weight: 600;
}

.welcome-div{
  color: #fff;
  letter-spacing: 0.09em;
}
//追加↑↑↑

Todoの入力、追加ボタンを作成を前にしましたがデータは登録されていません。

ここでは、Firestoreにデータを登録するところまで作ります。

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Home.jsの作成

勿論データを表示するため、もうお馴染みのuseStateを使います。

//追加↓↓↓
import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
//追加↓↓↓
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
//追加↑↑↑

export const Home = ({ currentUser }) => {

  //追加↓↓↓
    const [todo, setTodo] = useState('');
    const [todoError, setTodoError] = useState('');

    const handleTodoSubmit = async(e) => {
        e.preventDefault();
        await auth.onAuthStateChanged(user => {
            if (user) {
                addDoc(collection(db, 'todos of' + user.uid), {
                    Todo: todo
                }).then(setTodo('')).catch(err=>setTodoError(err.message))
            }
            else {
                console.log("user is not signed");
            }
        })
    }
  //追加↑↑↑

    return (
        <div className='wrapper'>
            <Header currentUser={currentUser} />
            <br />
            <br />
            <div className='container'>
                <form autoComplete='off' className='form-group'
          //追加↓↓↓
                    onSubmit={handleTodoSubmit}
                >

                    {currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required
              //追加↓↓↓
                            onChange={(e) => setTodo(e.target.value)}
                            value={todo}
                          //追加↑↑↑
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>

                    </>}

                    {!currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required disabled
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                disabled style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>
                        <div className='error-msg'>
                            Please register your account or login to use application
                        </div>
                    </>}

                </form>
        //追加↓↓↓
                {todoError && <div className='error-msg'></div>}
            </div>
        </div>
    )
}

Firestoreを確認してみましょう。データが追加されているはずです。

ここでは、先程Firestoreに追加したTodo情報を取得します。cssの作成ではレスポンシブ画面に対応させます。

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

App.jsの作成

import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
import { NotFound } from './Components/NotFound'
import { auth, db } from './services/firebase.config'
//変更・追加↓↓↓
import { doc, getDoc,query,where,collection,onSnapshot } from 'firebase/firestore';

export class App extends Component {

 
  state = {
  //変更・追加↓↓↓
    currentUser: null,
    todos:[]
  //変更・追加↑↑↑
  }

  componentDidMount() {
    auth.onAuthStateChanged(user => {
      if (user) {
        getDoc(doc(db, 'users', user.uid)).then(snapshot => {
          this.setState({
            currentUser: snapshot.data().userName
          })
        })
      }
      else {
        console.log("user is not signed")
      }
    })

     //追加↓↓↓
   auth.onAuthStateChanged(user => {
      if (user) {
        const todoList = [];
        const q = query(
          collection(db, 'todos of' + user.uid),
          where('userId', '==', user.uid)
        );
        const unsubscribe = onSnapshot(q, (snapshot) => {
          snapshot.docChanges().forEach((change) => {
            if (change.type === 'added') {
              todoList.push({
                id: change.doc.id,
                Todo: change.doc.data().Todo,
              });
            }
          });
          console.log('TODOリスト:', todoList);
          this.setState({ todos: todoList });
        });
        return unsubscribe;
      }
      else {
        console.log('user is not signed');
      }
    }); 
   //追加↑↑↑   
  }
 

  render() {
    return (
      <Router>
        <Routes>
          <Route path='/' element={
            <Home currentUser={this.state.currentUser}
          />} />
          <Route path='/signup' element={<Signup />} />
          <Route path='/login' element={<Login />} />
          <Route path='*' element={<NotFound />} />
        </Routes>
      </Router>
    )
  }
}

export default App

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

index.cssの作成

レスポンシブに対応させます。

* {
  margin: 0;
  padding: 0;
}

div.wrapper {
  overflow-x: hidden;
  overflow-y: auto;
}

/* header */
.header-box {
  width: 100%;
  height: auto;
  padding: 50px;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  background-color: #0170ad;
}

//追加↓↓↓
@media(max-width:768px){
  .header-box{
    flex-direction: column;
    justify-content: center;
  }
}
//追加↑↑↑

.header-box .leftside {
  flex: 1;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

//追加↓↓↓
@media(max-width: 768px){
  .header-box .leftside{
      width: 100%;
      flex-direction: column-reverse;
      justify-content: center;
      text-align: center;
      margin-bottom: 20px;
  }
}
//追加↑↑↑

.header-box .leftside .img {
  width: 170px;
  height: 170px;
}

.header-box .leftside .img img {
  width: 100%;
  height: 100%;
}

.header-box .leftside .content {
  color: #fff;
}

.header-box .leftside .content .heading-big {
  font-size: 42px;
}

.header-box .leftside .content .heading-small {
  font-size: 24px;
}

.header-box .rightside {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

//追加↓↓↓
@media(max-width:768px){
  .header-box .rightside{
    width: 100%;
    text-align: center;
  }
}
//追加↑↑↑

.header-box .rightside .btn, .header-box .rightside .btn:hover{
  width: 100px;
  margin-bottom: 5px;
  color: #fff;
  text-decoration: none;
}

.date-section{
  color: #fff;
}

.date-section span{
  margin-left: 4px;
}

/* sign up */
@media(max-width:539px){
  .register{
    width:100%
  }
}

.error-msg{
  color: red;
  width:100%;
  font-size:14px;
  font-weight: 600;
}

.welcome-div{
  color: #fff;
  letter-spacing: 0.09em;
}

F12ボタンを押してレスポンシブに対応をしているか確認してみてください。

ここでは、todo.jsを作成しtodo情報を表示することをしていきます。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Todo.jsの作成

  1. srcフォルダの中のComponentsフォルダに行く。
  2. 新規ファイルを作成し、Todos.jsと入力します。
  3. rafcと入力します
rafc
  1. 次のようになります。
import React from 'react'

export const Todos = () => {
    return (
        <div>Todos</div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Home.jsの作成

ホーム画面にTodo情報を表示するために、Todoの入力フォームの下に表示するコンポーネント(Todo.js)を追加します。

import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
//追加↓↓↓
import { Todos } from './Todos'

export const Home = ({ currentUser }) => {

    const [todo, setTodo] = useState('');
    const [todoError, setTodoError] = useState('');

    const handleTodoSubmit = async(e) => {
        e.preventDefault();
        await auth.onAuthStateChanged(user => {
            if (user) {
                addDoc(collection(db, 'todos of' + user.uid), {
                    Todo: todo,
                    userId: user.uid
                }).then(setTodo('')).catch(err=>setTodoError(err.message))
            }
            else {
                console.log("user is not signed");
            }
        })
    }

    return (
        <div className='wrapper'>
            <Header currentUser={currentUser} />
            <br />
            <br />
            <div className='container'>
                <form autoComplete='off' className='form-group'
                    onSubmit={handleTodoSubmit}
                >

                    {currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required
                            onChange={(e) => setTodo(e.target.value)}
                            value={todo}
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>

                    </>}

                    {!currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required disabled
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                disabled style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>
                        <div className='error-msg'>
                            Please register your account or login to use application
                        </div>
                    </>}

                </form>
                {todoError && <div className='error-msg'></div>}
                //追加↓↓↓
                <Todos/>
            </div>
        </div>
    )
}

Reactコンポーネントの作成

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Home.jsの作成

Todo情報を表示するために入力フォームの下に、表示するコンポーネントを配置します。

import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
import { Todos } from './Todos'

export const Home = ({ currentUser,todos }) => {

    const [todo, setTodo] = useState('');
    const [todoError, setTodoError] = useState('');

    const handleTodoSubmit = async(e) => {
        e.preventDefault();
        await auth.onAuthStateChanged(user => {
            if (user) {
                addDoc(collection(db, 'todos of' + user.uid), {
                    Todo: todo,
                    userId: user.uid
                }).then(setTodo('')).catch(err=>setTodoError(err.message))
            }
            else {
                console.log("user is not signed");
            }
        })
    }

    return (
        <div className='wrapper'>
            <Header currentUser={currentUser} />
            <br />
            <br />
            <div className='container'>
                <form autoComplete='off' className='form-group'
                    onSubmit={handleTodoSubmit}
                >

                    {currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required
                            onChange={(e) => setTodo(e.target.value)}
                            value={todo}
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>

                    </>}

                    {!currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required disabled
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                disabled style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>
                        <div className='error-msg'>
                            Please register your account or login to use application
                        </div>
                    </>}

                </form>
                {todoError && <div className='error-msg'></div>}
                //追加↓↓↓
                <Todos todos={todos}/>
            </div>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

IndividualTodo.jsの作成

表示する1つ分のTodoを作成します。

//追加↓↓↓
import React from 'react'
import {FiEdit} from 'react-icons/fi'
import {FaTrashAlt} from 'react-icons/fa'

export const IndividualTodo = ({ individualTodo }) => {
    return (
        <div className='todo'>
            <div>
                {individualTodo.Todo}
            </div>
            <div>
                <div className='actions-div'>
                    <FiEdit size={18} />
                </div>
                <div className='delete-btn'>
                    <FaTrashAlt size={18} />
                </div>
            </div>
        </div>
    )
}
//追加↑↑↑

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Todos.jsの作成

IndividualTodo.jsで作成した1つひとつのTodoを、map形式で順番に並べます。

//追加↓↓↓
import React from 'react'
import { IndividualTodo } from './IndividualTodo'

export const Todos = ({todos}) => {
    return todos.map((individualTodo)=>(
        <IndividualTodo
            individualTodo={individualTodo}
            key={individualTodo.id}
        />
    ))
}
//追加↑↑↑

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

index.cssの作成

Todo情報にCSSをつけて見栄えを整えます。

* {
  margin: 0;
  padding: 0;
}

div.wrapper {
  overflow-x: hidden;
  overflow-y: auto;
}

/* header */
.header-box {
  width: 100%;
  height: auto;
  padding: 50px;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  background-color: #0170ad;
}

@media(max-width:768px){
  .header-box{
    flex-direction: column;
    justify-content: center;
  }
}

.header-box .leftside {
  flex: 1;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

@media(max-width: 768px){
  .header-box .leftside{
      width: 100%;
      flex-direction: column-reverse;
      justify-content: center;
      text-align: center;
      margin-bottom: 20px;
  }
}

.header-box .leftside .img {
  width: 170px;
  height: 170px;
}

.header-box .leftside .img img {
  width: 100%;
  height: 100%;
}

.header-box .leftside .content {
  color: #fff;
}

.header-box .leftside .content .heading-big {
  font-size: 42px;
}

.header-box .leftside .content .heading-small {
  font-size: 24px;
}

.header-box .rightside {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

@media(max-width:768px){
  .header-box .rightside{
    width: 100%;
    text-align: center;
  }
}

.header-box .rightside .btn, .header-box .rightside .btn:hover{
  width: 100px;
  margin-bottom: 5px;
  color: #fff;
  text-decoration: none;
}

.date-section{
  color: #fff;
}

.date-section span{
  margin-left: 4px;
}

/* sign up */
@media(max-width:539px){
  .register{
    width:100%
  }
}

.error-msg{
  color: red;
  width:100%;
  font-size:14px;
  font-weight: 600;
}

.welcome-div{
  color: #fff;
  letter-spacing: 0.09em;
}

//追加↓↓↓
/* todo */
.todo{
  background-color: #e4e4e4;
  font-weight: 600;
  font-size: 16px; 
  margin: 10px 0px;
  padding: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.todo .actions-div{
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.todo .actions-div div{
  margin: 0px 10px;
  cursor: pointer;
}

.delete-btn{
  color: rgb(165, 2, 2);
  cursor: pointer;    
}
//追加↑↑↑

Reactコンポーネントの作成

ここでは、Todo情報の削除ボタンを機能させます。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

individualTodo.jsの作成

削除ボタンを追加しました。

import React from 'react'
import {FiEdit} from 'react-icons/fi'
import {FaTrashAlt} from 'react-icons/fa'

//追加↓↓↓
export const IndividualTodo = ({ individualTodo,deleteTodo }) => {

  //追加↓↓↓
    const handleDelete=()=>{
        // console.log(individualTodo.Todo);
        deleteTodo(individualTodo.id);
    }
  //追加↑↑↑

    return (
        <div className='todo'>
            <div>
                {individualTodo.Todo}
            </div>
            <div>
                <div className='actions-div'>
                    <FiEdit size={18} />
                </div>
                //追加↓↓↓
                <div className='delete-btn' onClick={handleDelete}>
                    <FaTrashAlt size={18} />
                </div>
            </div>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Todos.jsの作成

削除機能を格納します。

import React from 'react'
import { IndividualTodo } from './IndividualTodo'

//追加↓↓↓
export const Todos = ({todos,deleteTodo}) => {
    return todos.map((individualTodo)=>(
        <IndividualTodo
            individualTodo={individualTodo}
            key={individualTodo.id}
          //追加↓↓↓
            deleteTodo={deleteTodo}
        />
    ))
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Home.jsの作成

削除ボタンの機能を追加します。

import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
import { Todos } from './Todos'

//追加↓↓↓
export const Home = ({ currentUser, todos,deleteTodo }) => {

    const [todo, setTodo] = useState('');
    const [todoError, setTodoError] = useState('');

    const handleTodoSubmit = async (e) => {
        e.preventDefault();
        await auth.onAuthStateChanged(user => {
            if (user) {
                addDoc(collection(db, 'todos of' + user.uid), {
                    Todo: todo,
                    userId: user.uid
                }).then(setTodo('')).catch(err => setTodoError(err.message))
            }
            else {
                console.log("user is not signed");
            }
        })
    }

    return (
        <div className='wrapper'>
            <Header currentUser={currentUser} />
            <br />
            <br />
            <div className='container'>
                <form autoComplete='off' className='form-group'
                    onSubmit={handleTodoSubmit}
                >

                    {currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required
                            onChange={(e) => setTodo(e.target.value)}
                            value={todo}
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>

                    </>}

                    {!currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required disabled
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                disabled style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>
                        <div className='error-msg'>
                            Please register your account or login to use application
                        </div>
                    </>}

                </form>
                {todoError && <div className='error-msg'></div>}
                <Todos 
                    todos={todos}
                  //追加↓↓↓           
                    deleteTodo={deleteTodo}
                />
            </div>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

App.jsの作成

状態が変わったら、該当のTodoをReact上で削除するものと、Firestoreのデータベースから情報を削除するものを作ります。

import React, { Component } from 'react'
import './index.css'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Home } from './Components/Home'
import { Login } from './Components/Login'
import { Signup } from './Components/Signup'
import { NotFound } from './Components/NotFound'
import { auth, db } from './services/firebase.config'
//追加↓↓↓
import { doc, getDoc,deleteDoc,query,where,collection,onSnapshot } from 'firebase/firestore';

export class App extends Component {

  state = {
    currentUser: null,
    todos:[]
  }

  componentDidMount() {
    auth.onAuthStateChanged(user => {
      if (user) {
        getDoc(doc(db, 'users', user.uid)).then(snapshot => {
          this.setState({
            currentUser: snapshot.data().userName
          })
        })
      }
      else {
        console.log("user is not signed")
      }
    })

    auth.onAuthStateChanged(user => {
      if (user) {
        const todoList = [];
        const q = query(
          collection(db, 'todos of' + user.uid),
          where('userId', '==', user.uid)
        );
        const unsubscribe = onSnapshot(q, (snapshot) => {
          snapshot.docChanges().forEach((change) => {
            if (change.type === 'added') {
              todoList.push({
                id: change.doc.id,
                Todo: change.doc.data().Todo,
              });
            }
          //追加↓↓↓
            if(change.type==='removed'){
              //console.log(change.type);
              for(let i=0;i<todoList.length;i++){
                if(todoList[i].id === change.doc.id){
                  todoList.splice(i,1);
                }
              }
      //追加↑↑↑
            }
          });
          console.log('TODOリスト:', todoList);
          this.setState({ todos: todoList });
        });
        return unsubscribe;
      }
      else {
        console.log('user is not signed');
      }
    });    
  }

 //追加↓↓↓
  deleteTodo = (id) => {
    console.log(id);
    auth.onAuthStateChanged((user) => {
      if (user) {
        const docRef = doc(db, 'todos of' + user.uid, id);
        deleteDoc(docRef)
          .then(() => {
            console.log('Document successfully deleted!');
          })
          .catch((error) => {
            console.error('Error removing document: ', error);
          });
      } else {
        console.log('user is not signed');
      }
    });
  };
 //追加↑↑↑
  

  render() {
    return (
      <Router>
        <Routes>
          <Route path='/' element={<Home
            currentUser={this.state.currentUser}
            todos={this.state.todos}
      //追加↓↓↓
            deleteTodo={this.deleteTodo}
          />} />
          <Route path='/signup' element={<Signup />} />
          <Route path='/login' element={<Login />} />
          <Route path='*' element={<NotFound />} />
        </Routes>
      </Router>
    )
  }
}

export default App

Reactコンポーネントの作成

ここでは、編集する機能を作成します。

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Modal.jsの作成

編集用画面を作ります。

  1. srcフォルダの中のComponentsフォルダに行く。
  2. 新規ファイルを作成し、Signup.jsと入力します。
  3. rafcと記述して、
rafc
  1. 以下のようになるか確認してください。
import React from 'react'

export const Modal = () => {
    return (
        <div>Modal</div>
    )
}
  1. コードを記述します。
import React, { useState } from 'react'
import { FiXCircle } from 'react-icons/fi'
import { doc, updateDoc } from "firebase/firestore";
import { db, auth } from '../services/firebase.config'

export const Modal = ({ editTodoValue, editModal,updateTodoHandler }) => {

    const [editTodo, setEditTodo] = useState(editTodoValue.Todo);

    const handleClose = () => {
        editModal(null)
    }

    const handleEditTodoSubmit = async (e) => {
        e.preventDefault();
        handleClose();
        updateTodoHandler(editTodo, editTodoValue.id);
        await auth.onAuthStateChanged(user => {
            if (user) {
                const todoRef = doc(db, 'todos of' + user.uid,editTodoValue.id);
                updateDoc(todoRef, {
                    Todo: editTodo
                })

            }
            else {
                console.log("user is not signed");
            }
        })
    }

    return (
        <div className='modal-container'>
            <div className='modal'>
                <div className='header'>
                    <div className='update-text'>
                        Update your todo
                    </div>
                    <div className='close-btn'
                        onClick={handleClose}>
                        <FiXCircle size={28} />
                    </div>
                </div>
                <div className='container-fluid'>
                    <form autoComplete="off" className='form-group'
                        onSubmit={handleEditTodoSubmit}
                    >
                        <input type="text" className='form-control'
                            required placeholder="Update your todo"
                            value={editTodo}
                            onChange={(e)=>setEditTodo(e.target.value)}
                        />
                        <br />
                        <button type="submit" className='btn btn-success btn-lg'>
                            UPDATE
                        </button>
                    </form>
                </div>
            </div>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

individualTodo.jsの作成

編集ボタンを作成します。

import React from 'react'
import {FiEdit} from 'react-icons/fi'
import {FaTrashAlt} from 'react-icons/fa'

//追加↓↓↓
export const IndividualTodo = ({ individualTodo,deleteTodo,editModal }) => {

    const handleDelete=()=>{
        // console.log(individualTodo.Todo);
        deleteTodo(individualTodo.id);
    }
  
    //追加↓↓↓
    const handleEditModal=()=>{
        editModal(individualTodo);
    }
  //追加↑↑↑

    return (
        <div className='todo'>
            <div>
                {individualTodo.Todo}
            </div>
            <div className='actions-div'>
                 //追加↓↓↓
                <div onClick={handleEditModal}>
                    <FiEdit size={18} />
                </div>
                 //追加↓↓↓
                <div className='delete-btn' onClick={handleDelete}>
                    <FaTrashAlt size={18} />
                </div>
            </div>
        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Home.jsの作成

編集する機能をもつコンポーネントを追加します。

import React, { useState } from 'react'
import '../index.css'
import { Header } from './Header'
import { auth, db } from '../services/firebase.config'
import { collection, addDoc } from 'firebase/firestore'
import { Todos } from './Todos'
//追加↓↓↓
import { Modal } from './Modal'

//追加↓↓↓
export const Home = ({ currentUser,todos,deleteTodo,editTodoValue,editModal,updateTodoHandler }) => {

    const [todo, setTodo] = useState('');
    const [todoError, setTodoError] = useState('');

    const handleTodoSubmit = async (e) => {
        e.preventDefault();
        await auth.onAuthStateChanged(user => {
            if (user) {
                addDoc(collection(db, 'todos of' + user.uid), {
                    Todo: todo,
                    userId: user.uid
                }).then(setTodo('')).catch(err => setTodoError(err.message))
            }
            else {
                console.log("user is not signed");
            }
        })
    }

    return (
        <div className='wrapper'>
            <Header currentUser={currentUser} />
            <br />
            <br />
            <div className='container'>
                <form autoComplete='off' className='form-group'
                    onSubmit={handleTodoSubmit}
                >

                    {currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required
                            onChange={(e) => setTodo(e.target.value)}
                            value={todo}
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>

                    </>}

                    {!currentUser && <>
                        <input type="text" placeholder="Enter TODO's"
                            className='form-control' required disabled
                        />
                        <br />
                        <div style={{
                            width: 100 + '%',
                            display: 'flex', justifyContent: 'flex-end'
                        }}>
                            <button type="submit" className='btn btn-success'
                                disabled style={{ width: 100 + '%' }}>
                                ADD
                            </button>
                        </div>
                        <div className='error-msg'>
                            Please register your account or login to use application
                        </div>
                    </>}

                </form>
                {todoError && <div className='error-msg'></div>}
                <Todos 
                    todos={todos}
                    deleteTodo={deleteTodo}
                  //追加↓↓↓
                    editModal={editModal}
                />
            </div>

          //追加↓↓↓
            {editTodoValue && <Modal 
                editTodoValue={editTodoValue}
                editModal={editModal}
                updateTodoHandler={updateTodoHandler}
            />}
          //追加↑↑↑

        </div>
    )
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Todos.jsの作成

編集する機能をもつコンポーネントを追加します。

import React from 'react'
import { IndividualTodo } from './IndividualTodo'

//追加↓↓↓
export const Todos = ({todos,deleteTodo,editModal}) => {
    return todos.map((individualTodo)=>(
        <IndividualTodo
            individualTodo={individualTodo}
            key={individualTodo.id}
            deleteTodo={deleteTodo}
          //追加↓↓↓
            editModal={editModal}
        />
    ))
}

ここでは、今まで進めてきたものを実際にサイトで使えるようにします。

  1. 今までと同じ要領で、「ctrl + shift + @」で新しくコンソールを開きます。
  2. cd コマンドを用いてパッケージ化する場所を選択します。
  3. npm run build を実行します。
npm run build
  1. 完了したら firebase loginを実行してCLIの指示に従って、webでFirebase CLIにログインして、プロジェクトを作成したアカウントでログインします。
firebase login
  1. firebase init を使用して、firestoreとHosting(文章が長い方)を選択して次に行きます。
firebase init
  1. deploy先をpublicではなくbuildを選択して後は何も入力せずに、Enterを押して最後まで行きましょう。
  2. firebase serveをして、firebaseのローカル環境で実行できるか確認します。
firebase serve
  1. 問題がなければfirebase deployを実行して、公開します。
firebase deploy