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

#はじめに
react,reduxを学習もひと段落してきて調べていくうちにfirebase使ってサーバーレスなアプリが作れることがわかった．firebaseのいい教材はないものかと探しているうちに1つのYouTubeプレイリストを発見した．

[React, Redux & Firebase App Tutorial](https://www.youtube.com/playlist?list=PL4cUxeGkcC9iWstfXntcj8f-dFZ4UtlN3)

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

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

[React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発①]()　←今ここ！！！
[React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発②](https://qiita.com/momosuke/items/57295ca99387ab243ce8)
[React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発③](https://qiita.com/momosuke/items/a79564e2234d9e4fc0bd)
[React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発④](https://qiita.com/momosuke/items/0fa933f8b6e470e5796d)

#技術仕様
今回のアプリは以下のフレームワーク，サービスで開発される

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

ちなみに完成したものがこちら（3分クッキング）
[Momo Plan](https://momosuke-momoplan.firebaseapp.com)
適当にサインアップしてプロジェクトを追加してみてください．
(テスト用アカウント：email: test@gmail.com, password: test1234)

#事前知識
以下の知識があると記事の内容をスムーズに理解できる．
- Reactの基礎
- Reduxの基礎
- JavaScript & HTML

#全体図
<img width="1024" alt="IMG_7E516210EBC3-1.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/8c794fe5-7c9e-7698-c588-b4945b39e9a9.jpeg">

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

下画像は各コンポネントの概略図．
![IMG_0089.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/4d525317-09b6-888e-5d75-c47ac2a0f7aa.png)

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

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

#セットアップ
今回はコードエディタに[Visual Studio Code](https://azure.microsoft.com/ja-jp/products/visual-studio-code/)を利用する．エディタ内にターミナルを展開できるのが便利．

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

```terminal.
npx create-react-app marioplan
cd marioplan
npm start
```
`create-react-app`で作られたダミープロジェクトが表示されればひとまずOK．
<img width="1680" alt="スクリーンショット 2019-10-21 22.53.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/54ea7ee0-e13d-386c-b5cc-32d2f33baf8e.png">

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

```src/App.js
import React from 'react';

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

export default App;
```

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

```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>
            
```

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

修正：ProjectDetail.js → ProjectDetails.js　
<img width="167" alt="スクリーンショット 2019-10-21 23.33.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/6eede91e-7765-3836-9a32-2b7c340b2a1c.png">

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

```
cd marioplan
npm install react-router-dom
```

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

```src/App.js
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;
```
次にNavbar.jsを書き込む．

```layout/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;
```
App.jsで読み込む．

```src/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;
```

`npm start`で確認するとこんな感じ．Materialized cssがいい仕事してる．
<img width="1677" alt="スクリーンショット 2019-10-22 14.04.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/a5807238-f2e7-1703-0c43-f372dc46b198.png">

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

```layout/SignedInLinks.js
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;
```

```layout/SignedOutLinks.js
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;
```
Navbar.jsで読み込む

```layout/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;
```
<img width="1677" alt="スクリーンショット 2019-10-22 14.19.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/1be43ca4-ee6c-c756-0700-dc553111f60f.png">

#Dashboardコンポネント
次にDashboardを作っていく．その前にラップするProjectList,Notificationsを書いていく．

```projects/ProjectList.js
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;
```

```dashboard/Notifications.js
import React from 'react'

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

export default Notification;
```

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

```dashboard/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;
```
このDashboardコンポネントをApp.jsで読み込む．`Switch`を使えばルート管理ができる．タグ内で`Route`タグで各ルートのpathとレンダリングするcomponentを設置できる．

```src/App.js
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;
```
`npm start`するとこんな感じ．

<img width="1677" alt="スクリーンショット 2019-10-22 15.29.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/494a713c-85e9-d8e4-ef7e-236a34d2b352.png">

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

```projects/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;
```
ProjectList.jsで読み込む．

```projects/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;
```

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

```projects/ProjectDetails.js
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;
```
App.jsで読み込む．
注意点として，pathが`/`のところに`exact`を付けないと`/project/3`のようなものにも反応して`Dashboard`に遷移してしまう．
またpathが`/project/:id`で`id`を受け取っている．これによりプロジェクトの分別を可能にする．

```src/App.js
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;
```

pathを`/project/3`での画面はこんな感じ．（とりあえず今はidをproject title後ろに反映させておくだけ）
<img width="1677" alt="スクリーンショット 2019-10-22 17.05.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/b160cd07-cbd9-2d54-44dc-5b32e21f7a89.png">

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

```auth/SignIn.js
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
```

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

```src/App.js
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;
```
<img width="1677" alt="スクリーンショット 2019-10-22 17.42.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/42a62d79-4bea-4429-4985-1ce497599e32.png">
<img width="1677" alt="スクリーンショット 2019-10-22 17.43.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/da298ca3-8df4-ad31-3dc3-108b2ae84e2a.png">

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

```projects/CreateProject.js
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
```

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

```src/App.js
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;
```

<img width="1677" alt="スクリーンショット 2019-10-22 18.04.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/55d727db-d6ad-3c57-a9b0-b58aa517adcd.png">

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

```SignedInLinks.js
<li><NavLink to='/create'>New Project</NavLink></li>
```

```SignedOutLinks.js
<li><NavLink to='/signup'>Signup</NavLink></li>
<li><NavLink to='/signin'>Log In</NavLink></li>
```

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

背景に使う画像を[ここ](https://github.com/iamshaunjp/React-Redux-Firebase-App/tree/lesson-10/marioplan/public/img)からダウンロードして`public`に`img`フォルダを作ってそこに配置．

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

```src/index.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;
}
```
<img width="1677" alt="スクリーンショット 2019-10-22 21.10.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331294/f3d99014-fbae-4ba9-ab71-27c6e373e365.png">


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