#はじめに
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
クラインアントサイドで動くReduxを使ってReactアプリを作成していき,リアルタイムデータをFirebase(Firestore db)に保管していく.
Firebase Authを使ってログイン機能を実装し,最後にサーバー側で実行される関数を設定できるCloud Fuctionを利用する.
create-react-appでrootコンポネントを作成し,全リンク共通のNavコンポネントを作成.その中にはログイン中に表示したいSigned in linksとその逆のSigned out linksを作成する.
その後ルート別のメインコンポネントを作成していくという流れで今回はアプリの外面をReactで作っていく.
#セットアップ
今回はコードエディタにVisual Studio Codeを利用する.エディタ内にターミナルを展開できるのが便利.
それではアプリを作っていく.create-react-app
をインストールしなくてもいいようにnpm
でなくnpx
を使う.
npx create-react-app marioplan
cd marioplan
npm start
create-react-app
で作られたダミープロジェクトが表示されればひとまずOK.
次にrootコンポネントであるApp.jsを開き,いらないところを削除して下記のようにする.
そしてsrc/App.cssも削除.
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に以下のコードを挿入.
<!--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
ルート管理のためにreact-router-domをインストールする.
cd marioplan
npm install react-router-dom
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;
次に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で読み込む.
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がいい仕事してる.
#SignedInLinks & SignedOutLinksコンポネント
次に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;
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で読み込む
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;
#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;
import React from 'react'
const Notification = () => {
return (
<div>
// 通知の表示はfirebaseとの連携が必要なので結構後半で構うまでは適当にpタグで我慢...
<p>Notification</p>
</div>
)
}
export default Notification;
これらを読み込む形で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を設置できる.
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
するとこんな感じ.
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;
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)
などで確認してみよう.
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
を受け取っている.これによりプロジェクトの分別を可能にする.
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後ろに反映させておくだけ)
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
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()
はボタンが押されてもページがリロードされないためのもの.
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;
#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
今までと同様に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;
作成したNavbarのリンクたちが正しく遷移するようにSignedInlinksと SignedOutLinksを修正しよう.
<li><NavLink to='/create'>New Project</NavLink></li>
<li><NavLink to='/signup'>Signup</NavLink></li>
<li><NavLink to='/signin'>Log In</NavLink></li>
ここで少しオリジナルにCSSをいじっていく.
背景に使う画像をここからダウンロードしてpublic
にimg
フォルダを作ってそこに配置.
そして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;
}
これでだいぶ見栄えは完成しました.次からいよいよReduxの状態管理を使ってfirebaseとの連携に備える.