React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発①

1/6/2020 ReactReduxFirebase

はじめに

react,reduxを学習もひと段落してきて実際に開発しようと思ったが,機械系高専生はバックエンドを知らなかった.そこで調べていくうちにfirebase使ってサーバーレスなアプリが作れることがわかった. firebaseのいい教材はないものかと探しているうちに1つのYouTubeプレイリストを発見した.

React, Redux & Firebase App Tutorial (opens new window)

何を隠そうこの記事はこのプレイリストを翻訳・まとめたものである.英語が苦手な自分には40もある再生リストを一時停止しながら進めるのはすごく時間がかかった.なので同族のためにも僕がやっておこうと思った.自分偉いなんて1ピコも思ってない.

かなり長くなるのでプレイリスト10ずつで記事も区切っていくことにした.

React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発① ←今ここ!!!

React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発②

React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発③

React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発④

技術仕様

今回のアプリは以下のフレームワーク,サービスで開発される

  • React & Redux
  • Thunk
  • Firebase
  • Cloud Firestore
  • Authentication
  • Hosting
  • Cloud Function

ちなみに完成したものがこちら(3分クッキング) Momo Plan (opens new window)

適当にサインアップしてプロジェクトを追加してみてください. (テスト用アカウント:email: test@gmail.com, password: test1234)

事前知識

以下の知識があると記事の内容をスムーズに理解できる.

  • Reactの基礎
  • Reduxの基礎
  • JavaScript & HTML

全体図

IMG_7E516210EBC3-1.jpeg

クラインアントサイドで動くReduxを使ってReactアプリを作成していき,リアルタイムデータをFirebase(Firestore db)に保管していく. Firebase Authを使ってログイン機能を実装し,最後にサーバー側で実行される関数を設定できるCloud Fuctionを利用する.

下画像は各コンポネントの概略図. IMG_0089.PNG

create-react-appでrootコンポネントを作成し,全リンク共通のNavコンポネントを作成.その中にはログイン中に表示したいSigned in linksとその逆のSigned out linksを作成する.

その後ルート別のメインコンポネントを作成していくという流れで今回はアプリの外面をReactで作っていく.

セットアップ

今回はコードエディタにVisual Studio Code (opens new window)を利用する.エディタ内にターミナルを展開できるのが便利.

それではアプリを作っていく.create-react-appをインストールしなくてもいいようにnpm でなくnpxを使う.

npx create-react-app marioplan
cd marioplan
npm start
1
2
3

create-react-appで作られたダミープロジェクトが表示されればひとまずOK. スクリーンショット 2019-10-21 22.53.38.png

次にrootコンポネントであるApp.jsを開き,いらないところを削除して下記のようにする. そしてsrc/App.cssも削除.

import React from 'react';

function App() {
  return (
    <div className="App">
      <h1>Mario Plan</h1>
    </div>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11

CSSに注力したくないのでMaterializedCSS (opens new window)を使う. これから出てくるコンポネントのクラス名classNameはほぼ全てMaterialized CSS用のものなので詳しく知りたい人は公式ドキュメントを参照推奨. public/index.htmlに以下のコードを挿入.

<!--Import Google Icon Font-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!--Import materialize.css-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">

↑ は<title>の直前に挿入
↓ は<body>の最後に挿入

<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
            
1
2
3
4
5
6
7
8
9
10
11

ここからコンポネントを作っていく. srcフォルダにcomponentsフォルダを作成し,以下のように諸々のフォルダや空ファイルを作成しておく.

修正:ProjectDetail.js → ProjectDetails.js  スクリーンショット 2019-10-21 23.33.15.png

ルート管理のためにreact-router-domをインストールする.

cd marioplan
npm install react-router-dom
1
2

App.js内でインポートして中身も少しいじる.BrowserRouterで囲むことによって今後追加するルート処理が可能になる.

import React, { Component } from 'react';
import { BrowserRouter } from 'react-router-dom';

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div className="App">
          <h1>Mario Plan</h1>
        </div>
      </BrowserRouter>
    );
  }
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

次にNavbar.jsを書き込む.

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

const Navbar = () => { 
    return (
        <nav className="nav-wrapper grey darken-3"> 
            <div className="container">
                <Link to='/' className="brand-logo">MarioPlan</Link> // 画面遷移のためのタグ
            </div>
        </nav>
    )
}

export default Navbar;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

App.jsで読み込む.

import React, { Component } from 'react'
import { BrowserRouter } from 'react-router-dom'
import Navbar from './components/layout/Navbar'

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div className="App">
          <Navbar />
        </div>
      </BrowserRouter>
    );
  }
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

npm startで確認するとこんな感じ.Materialized cssがいい仕事してる. スクリーンショット 2019-10-22 14.04.58.png

次にNavbarに表示するコンポネントとしてログインしている時に見えていて欲しいSignedInLinksと,していないときに見えて欲しいSignedOutLinksを書いていく.まずは両方表示して後半で改良していく.

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

const SignedInLinks = () => {
    return (
            <ul className="right">
            <li><NavLink to='/'>New Project</NavLink></li>
            <li><NavLink to='/'>Log Out</NavLink></li>
            // 作業段階にて下の行で'NN'を忘れたので下画像で円内に'NN'が表示されてませんが
            // 表示されているのが正常なので悪しからず.
            <li><NavLink to='/' className="btn btn-floating pink lighten-1">NN</NavLink></li>
        </ul>
    )
}

export default SignedInLinks;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'
import { NavLink } from 'react-router-dom'

const SignedOutLinks = () => {
    return (
            <ul className="right">
            <li><NavLink to='/'>Signup</NavLink></li>
            <li><NavLink to='/'>Log In</NavLink></li>
        </ul>
    )
}

export default SignedOutLinks;
1
2
3
4
5
6
7
8
9
10
11
12
13

Navbar.jsで読み込む

import React from 'react'
import { Link } from 'react-router-dom'
import SignedInLinks from './SignedInLinks'
import SignedOutLinks from './SignedOutLinks'


const Navbar = () => {
    return (
        <nav className="nav-wrapper grey darken-3">
            <div className="container">
                <Link to='/' className="brand-logo">MarioPlan</Link>
                <SignedInLinks />
                <SignedOutLinks />
            </div>
        </nav>
    )
}

export default Navbar;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
スクリーンショット 2019-10-22 14.19.26.png

Dashboardコンポネント

次にDashboardを作っていく.その前にラップするProjectList,Notificationsを書いていく.

import React from 'react'

const ProjectList = () => {
    return (
        <div className="project-list section">

            // ダミープロジェクトを3つ作っておく.

            <div className="card z-depth-0 project-summary">
                <div className="card-content grey-text text-darken-3">
                    <span className="card-title">Project Title</span>
                    <p>Posted by the Net Ninja</p>
                    <p className="grey-text">3rd September</p>
                </div>
            </div>

            <div className="card z-depth-0 project-summary">
                <div className="card-content grey-text text-darken-3">
                    <span className="card-title">Project Title</span>
                    <p>Posted by the Net Ninja</p>
                    <p className="grey-text">3rd September</p>
                </div>
            </div>

            <div className="card z-depth-0 project-summary">
                <div className="card-content grey-text text-darken-3">
                    <span className="card-title">Project Title</span>
                    <p>Posted by the Net Ninja</p>
                    <p className="grey-text">3rd September</p>
                </div>
            </div>

        </div>
    )
}

export default ProjectList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react'

const Notification = () => {
    return (
        <div>
       // 通知の表示はfirebaseとの連携が必要なので結構後半で構うまでは適当にpタグで我慢...
            <p>Notification</p>
        </div>
    )
}

export default Notification;
1
2
3
4
5
6
7
8
9
10
11
12

これらを読み込む形でDashboard.jsを作成していく.

import React, { Component } from 'react'
import Notification from './Notification'
import ProjectList from '../projects/ProjectList'

class Dashboard extends Component {
    render() {
        return (
            <div className="dashboard container">
                <div className="row">
                    <div className="col s12 m6">
                        <ProjectList />
                    </div>
                    <div className="col s12 m5 offset-m1">
                        <Notification />
                    </div>
                </div>
            </div>
        )
    }
}

export default Dashboard;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

このDashboardコンポネントをApp.jsで読み込む.Switchを使えばルート管理ができる.タグ内でRouteタグで各ルートのpathとレンダリングするcomponentを設置できる.

import React, { Component } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/layout/Navbar'
import Dashboard from './components/dashboard/Dashboard'

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div className="App">
          <Navbar />
          <Switch>
            <Route path='/' component={Dashboard} />
          </Switch>
        </div>
      </BrowserRouter>
    );
  }
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

npm startするとこんな感じ.

スクリーンショット 2019-10-22 15.29.26.png

ProjectList.jsでプロジェクトが羅列してあるのは非効率なのでProjectSummary.jsにモジュール化していく.

import React from 'react'

const ProjectSummary = () => {
    return (
        <div className="card z-depth-0 project-summary">
            <div className="card-content grey-text text-darken-3">
                <span className="card-title">Project Title</span>
                <p>Posted by the Net Ninja</p>
                <p className="grey-text">3rd September</p>
            </div>
        </div>
    )
}

export default ProjectSummary;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

ProjectList.jsで読み込む.

import React from 'react'
import ProjectSummary from './ProjectSummary'

const ProjectList = () => {
    return (
        <div className="project-list section">
            <ProjectSummary />
            <ProjectSummary />
            <ProjectSummary />
            <ProjectSummary />
        </div>
    )
}

export default ProjectList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

ProjectDetailsコンポネント

次にプロジェクトをクリックしたら表示されるProjectDetailコンポネントを作成していく. このコンポネントには/project/:idというpathでアクセスするのだが,propsにはそのidパラメータが渡される.今回はprops.match.params.idで取得できる.他に参照したい値があれば適宜console.log(props)などで確認してみよう.

import React from 'react'
import ProjectSummary from './ProjectSummary'

const ProjectDetails = (props) => {
    const id = props.match.params.id;
    return (
        <div className="container section project-details">
            <div className="card z-depth-0">
                <div className="card-content">
                    <span className="card-title">Project Title - {id}</span>
                    <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptatibus consectetur, adipisci in, corrupti corporis omnis, maxime assumenda nisi expedita eius libero tempora totam officiis. Tenetur repellat accusamus excepturi aspernatur sint?</p>
                </div>
                <div className="card-action gret lighten-4 grey-text">
                    <div>Posted by The Net Ninja</div>
                    <div>2nd, September, 2am</div>
                </div>
            </div>
        </div>
    )
}

export default ProjectDetails;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

App.jsで読み込む. 注意点として,pathが/のところにexactを付けないと/project/3のようなものにも反応してDashboardに遷移してしまう. またpathが/project/:ididを受け取っている.これによりプロジェクトの分別を可能にする.

import React, { Component } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/layout/Navbar'
import Dashboard from './components/dashboard/Dashboard'
import ProjectDetails from './components/projects/ProjectDetails'

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div className="App">
          <Navbar />
          <Switch>
            <Route exact path='/' component={Dashboard} />
            <Route path='/project/:id' component={ProjectDetails} />
          </Switch>
        </div>
      </BrowserRouter>
    );
  }
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

pathを/project/3での画面はこんな感じ.(とりあえず今はidをproject title後ろに反映させておくだけ) スクリーンショット 2019-10-22 17.05.31.png

SignIn & SignUpコンポネント

次にSignIn, SignUpコンポネントを作成していく.

import React, { Component } from 'react'

class SignIn extends Component {
    state = {
        email: '',
        password: ''
    }
    handleChange = (e) => {
        this.setState({
            [e.target.id]: e.target.value
        })
    }
    handleSubmit = (e) => {
        e.preventDefault()
        console.log(this.state)
    }
    render() {
        return (
            <div className="container">
                <form onSubmit={this.handleSubmit} className="white">
                    <h5 className="grey-text text-darken-3">Sign In</h5>
                    <div className="input-field">
                        <label htmlFor="email">Email</label>
                        <input type="email" id="email" onChange={this.handleChange}/>
                    </div>
                    <div className="input-field">
                        <label htmlFor="password">Password</label>
                        <input type="password" id="password" onChange={this.handleChange}/>
                    </div>
                    <div className="input-field">
                        <button className="btn pink lighten-1 z-depth-0">Login</button>
                    </div>
                </form>
            </div>
        )
    }
}

export default SignIn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { Component } from 'react'

class SignUp extends Component {
    state = {
        email: '',
        password: '',
        firstName: '',
        lastName: ''
    }
    handleChange = (e) => {
        this.setState({
            [e.target.id]: e.target.value
        })
    }
    handleSubmit = (e) => {
        e.preventDefault()
        console.log(this.state)
    }
    render() {
        return (
            <div className="container">
                <form onSubmit={this.handleSubmit} className="white">
                    <h5 className="grey-text text-darken-3">Sign Up</h5>
                    <div className="input-field">
                        <label htmlFor="email">Email</label>
                        <input type="email" id="email" onChange={this.handleChange}/>
                    </div>
                    <div className="input-field">
                        <label htmlFor="password">Password</label>
                        <input type="password" id="password" onChange={this.handleChange}/>
                    </div>
                    <div className="input-field">
                        <label htmlFor="firstName">First Name</label>
                        <input type="text" id="firstName" onChange={this.handleChange}/>
                    </div>
                    <div className="input-field">
                        <label htmlFor="lastName">Last Name</label>
                        <input type="text" id="lastName" onChange={this.handleChange}/>
                    </div>
                    <div className="input-field">
                        <button className="btn pink lighten-1 z-depth-0">Sign up</button>
                    </div>
                </form>
            </div>
        )
    }
}

export default SignUp;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

どちらも入力内容をstateで保管するようにしている.送信ボタンでconsole.logされるようにしたのでApp.jsで読み込んで適当にsign in, sign upしてコンソール画面を確認してみよう.今後ログに出すだけでなく.firestoreに保管するように改良する.ちなみにpreventDefault()はボタンが押されてもページがリロードされないためのもの.

import React, { Component } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/layout/Navbar'
import Dashboard from './components/dashboard/Dashboard'
import ProjectDetails from './components/projects/ProjectDetails'
import SignIn from './components/auth/SignIn'
import SignUp from './components/auth/SignUp'

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div className="App">
          <Navbar />
          <Switch>
            <Route exact path='/' component={Dashboard} />
            <Route path='/project/:id' component={ProjectDetails} />
            <Route path='/signin' component={SignIn} />
            <Route path='/signup' component={SignUp} />
          </Switch>
        </div>
      </BrowserRouter>
    );
  }
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
スクリーンショット 2019-10-22 17.42.49.png スクリーンショット 2019-10-22 17.43.14.png

CreateProjectコンポネント

次にCreateProjectコンポネントを作成していく.

import React, { Component } from 'react'

class CreateProject extends Component {
    state = {
        title: '',
        content: ''
    }
    handleChange = (e) => {
        this.setState({
            [e.target.id]: e.target.value
        })
    }
    handleSubmit = (e) => {
        e.preventDefault()
        console.log(this.state)
    }
    render() {
        return (
            <div className="container">
                <form onSubmit={this.handleSubmit} className="white">
                    <h5 className="grey-text text-darken-3">Create new project</h5>
                    <div className="input-field">
                        <label htmlFor="title">Title</label>
                        <input type="text" id="title" onChange={this.handleChange} />
                    </div>
                    <div className="input-field">
                        <label htmlFor="content">Project Content</label>
                        <textarea id="content" className="materialize-textarea" onChange={this.handleChange}></textarea>
                    </div>
                    <div className="input-field">
                        <button className="btn pink lighten-1 z-depth-0">Create</button>
                    </div>
                </form>
            </div>
        )
    }
}

export default CreateProject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

今までと同様にApp.jsで読み込んで/createにアクセスしてみよう.

import React, { Component } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/layout/Navbar'
import Dashboard from './components/dashboard/Dashboard'
import ProjectDetails from './components/projects/ProjectDetails'
import SignIn from './components/auth/SignIn'
import SignUp from './components/auth/SignUp'
import CreateProject from './components/projects/CreateProject'

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div className="App">
          <Navbar />
          <Switch>
            <Route exact path='/' component={Dashboard} />
            <Route path='/project/:id' component={ProjectDetails} />
            <Route path='/signin' component={SignIn} />
            <Route path='/signup' component={SignUp} />
            <Route path='/create' component={CreateProject} />
          </Switch>
        </div>
      </BrowserRouter>
    );
  }
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
スクリーンショット 2019-10-22 18.04.21.png

作成したNavbarのリンクたちが正しく遷移するようにSignedInlinksと SignedOutLinksを修正しよう.

<li><NavLink to='/create'>New Project</NavLink></li>
1
<li><NavLink to='/signup'>Signup</NavLink></li>
<li><NavLink to='/signin'>Log In</NavLink></li>
1
2

ここで少しオリジナルにCSSをいじっていく.

背景に使う画像をここ (opens new window)からダウンロードしてpublicimgフォルダを作ってそこに配置.

そしてsrc/index.cssでcssをいじる.

// 画像の底が画面の底にくるための設定
html {
  min-height: 100%;
}
// 背景設定
body {
  margin: 0;
  padding: 0rem;
  font-family: sans-serif;
  background: url(/img/mario-bg.png) no-repeat;
  background-size: 100%;
  background-position: bottom;
  background-color: #95e8f3;
  min-height: 100%;
}

form {
  padding: 20px;
  margin-top: 60px;
}

form button, form h5 {
  margin: 20px 0;
}
// フォームが選択状態の時にボタンと同じ色になる設定
input[type=text]:not(.browser-default):focus:not([readonly]),
input[type=email]:not(.browser-default):focus:not([readonly]),
input[type=password]:not(.browser-default):focus:not([readonly]),
textarea.materialize-textarea:focus:not([readonly]) {
  border-color: #ec407a;
  box-shadow: none;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
スクリーンショット 2019-10-22 21.10.32.png

これでだいぶ見栄えは完成しました.次からいよいよReduxの状態管理を使ってfirebaseとの連携に備える.

Last Updated: 4/11/2021, 1:24:08 PM