55
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2019-10-22

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

React, Redux & Firebase App Tutorial

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

かなり長くなるのでプレイリスト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
適当にサインアップしてプロジェクトを追加してみてください.
(テスト用アカウント: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を利用する.エディタ内にターミナルを展開できるのが便利.

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

terminal.
npx create-react-app marioplan
cd marioplan
npm start

create-react-appで作られたダミープロジェクトが表示されればひとまずOK.
スクリーンショット 2019-10-21 22.53.38.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を使う.
これから出てくるコンポネントのクラス名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 
スクリーンショット 2019-10-21 23.33.15.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がいい仕事してる.
スクリーンショット 2019-10-22 14.04.58.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;
スクリーンショット 2019-10-22 14.19.26.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するとこんな感じ.

スクリーンショット 2019-10-22 15.29.26.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/:ididを受け取っている.これによりプロジェクトの分別を可能にする.

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後ろに反映させておくだけ)
スクリーンショット 2019-10-22 17.05.31.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;
スクリーンショット 2019-10-22 17.42.49.png スクリーンショット 2019-10-22 17.43.14.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;
スクリーンショット 2019-10-22 18.04.21.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をいじっていく.

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

そして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;
}
スクリーンショット 2019-10-22 21.10.32.png

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

55
56
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?