前回ではReduxで状態管理を設定し,Firebaseと接続,firestoreデータベースとの同期まで漕ぎ着けた.
今回はFirebase Authenticationを使ってログイン機能を実装していきたい.
React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発①
React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発②
React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発③ ←今ここ!!!
React + Redux + Firebase を使ってログイン機能あり掲示板アプリ開発④
Firebase Authenticationイントロ
Firebase Authenticationは認証機能を司る機能でFirestoreなどと連動することが可能.
また一種のデータベースのような振る舞いもする.ユーザーが登録した時にユーザー情報を保管する.firestoreデータベースにではなく.
例えばメールアドレスやユーザーID(Authenticationによる自動生成)などが保管できる.しかし自由度は低く,名前や,パスワードなどのカスタムプロパティは保存できない.
なのでそれらカスタムプロパティはfirestoreにUsersコレクションを作成し,そこに保存することにする.
そこにAuthenticationで作成したIDでもってデータを保管すればAuthentication内のデータとの結びつけもできることになる.

まずはFirebase Authenticationを利用可能にしてみよう.
firebaseのダッシュボードからAuthenticationページに行って,ログイン方法を設定をクリック,
FacebookやTwitterなどたくさんのログインメソッドが出てくる.今回は単純にメール/パスワードを有効にする.(メールリンクは無効のままで)
こいつはまだ追加の情報を持ってないし,firestoreで連携していない.
最終的にはUsersコレクションを作って,ユーザと連携するようにしたい.
ReduxとFirebase Authの接続
Firebase Authの設定が完了したので次はアプリ内で使えるようにしたい.具体的にはユーザーのAuthenticationステータスを常に追っている状態にしたい.ログインしたのか,ログアウトしたのか.というようなステータスを.
そのためにはfirebaseからFirebase Authenticationのステータスをreduxのstoreに同期してstateからステータスを確認できるようにする必要がある.
前回,firestoreReducerを使って,firestoreとstateを同期した.今回も似たようなことをすればいい.
ということでrootReducerにfirebaseReducerを追加する.
import authReducer from './authReducer'
import projectReducer from './projectReducer'
import { combineReducers } from 'redux'
import { firestoreReducer } from 'redux-firestore'
import { firebaseReducer } from 'react-redux-firebase'
const rootReducer = combineReducers({
auth: authReducer,
project: projectReducer,
firestore: firestoreReducer,
firebase: firebaseReducer
})
export default rootReducer
firebaseReducerによってfirebaseの全てのステータスがreduxひいてはstateに同期される.stateのfirebaseオブジェクトにね.
これでセットアップは完了した.では実際にコンポネントからAuthenticationステータスにアクセスしてみよう.
今回はNavbarコンポネントでアクセスしていく.ここではSignedInLinksとSignedOutLinksコンポネントが呼び出されているが,①で書いたように本来はログイン状況に応じて片方のみを表示したいのだ.
まずはコンポネントをconnectを使ってReduxを接続している.ここではfirestoreのデータはいらない.つまりHOCは1つなのでcomposeはいらない.
とりあえずどんな形式でstateに保存されているのか知るためにconsole.log(state)だけしてみる.
import React from 'react'
import { Link } from 'react-router-dom'
import SignedInLinks from './SignedInLinks'
import SignedOutLinks from './SignedOutLinks'
import { connect } from 'react-redux'
const Navbar = () => {
return (
<nav className="nav-wrapper grey darken-3">
<div className="container">
<Link to='/' className="brand-logo">MarioPlan</Link>
<SignedInLinks />
<SignedOutLinks />
</div>
</nav>
)
}
const mapStateToProps = (state) => {
console.log(state);
return {
}
}
export default connect(mapStateToProps)(Navbar);
実行してコンソールから確認してみると,

firebaseのauthプロパティをみるとisEmptyがtrueになっている.これはプロフィールが存在しないことを表している.つまりログインしていないことを表す.
次はログイン機能を実装していく.
ログイン機能
メールアドレスとパスワードを使ってログインするにはfirebaseプロジェクトとの非同期処理を行う必要がある.
それはどこでやればいいか.もうわかるよね.action creatorだ.
ということでサインインに関するaction creatorを作成していく.
おさらいだけどthunkのおかげでdispatchを一旦止めて代わりに関数を返すことができたよね.そこでdispatchやfirebaseに関するインスタンスを作るgetFirebaseを受け取って非同期処理が行えた.
今回はauth関数,そしてsignInWithEmailAndPasswordメソッドを使って引数に必要な情報を渡すことでログインできる.
そしてprojectActions.js同様,非同期処理には幾分か時間がかかることを考慮してthenメソッドを使ってコールバック関数を呼び出し,その中でactionをdispatchする.
export const signIn = (credentials) => {
return (dispatch, getState, {getFirebase}) => {
const firebase = getFirebase();
firebase.auth().signInWithEmailAndPassword(
credentials.email,
credentials.password
).then(() => {
dispatch({ type: 'LOGIN_SUCCESS' });
}).catch((err) => {
dispatch({ type: 'LOGIN_ERROR' });
});
}
}
credentialsにはメールアドレスやパスワードが含まれる.
次にこれらのactionを扱うreducerをauthReducer.jsで実装する.
ログインが成功したかどうかという指標としてauthErrorというプロパティを与えた.
const initState = {
authError: null
}
const authReducer = (state = initState, action) => {
switch(action.type) {
case 'LOGIN_ERROR':
console.log('login error');
return {
...state,
authError: 'Login failed'
}
case 'LOGIN_SUCCESS':
console.log('login success');
return {
...state,
authError: null
}
default:
return state;
}
}
export default authReducer
注意としてrootReducerを見ればわかるがここでのstateはreduxストアのstate.authに対応する.state.firebaseではなくて.
あとはSignInコンポネントからauthActionsアクションクリエータをdispatchすればいい.
そのためにはauthActionsを孕んだdisaptchをpropsとしてコンポネントに渡す.方法はもちろんconnnect.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { signIn } from '../../store/actions/authActions'
class SignIn extends Component {
state = {
email: '',
password: ''
}
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault()
this.props.signIn(this.state)
}
render() {
const { authError } = this.props;
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 className="red-text center">
{ authError ? <p>{authError}</p> : null }
</div>
</div>
</form>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
authError: state.auth.authError
}
}
const mapDispatchToProps = (dispatch) => {
return {
signIn: (creds) => dispatch(signIn(creds))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SignIn)
エラーがあったとき用にmapStateToPropsでauthErrorを渡してフォーム下部でログイン失敗するとauthErrorが表示されるようにした.

わざとログイン失敗してauthErrorが表示されるのも確認してみて欲しい.
次はログアウトを実装したいと思う.
ログアウト機能
ログアウト用のコンポネントは作ってなかったよね.
SignedInkLinksのLog Outをクリックすると即座にログアウトする.そんな設計だった.今はクリックしてもダッシュボードに飛ぶだけ.
ログアウトもログイン同様,非同期処理でもって遂行する.ということは実装するのはもちろんaction creator上だ.
ということでauthActionsをいじってログアウト用のaction creatorを書いていく.
export const signIn = (credentials) => {
return (dispatch, getState, {getFirebase}) => {
const firebase = getFirebase();
firebase.auth().signInWithEmailAndPassword(
credentials.email,
credentials.password
).then(() => {
dispatch({ type: 'LOGIN_SUCCESS' });
}).catch((err) => {
dispatch({ type: 'LOGIN_ERROR' });
});
}
}
export const signOut = () => {
return (dispatch, getState, {getFirebase}) => {
const firebase = getFirebase();
firebase.auth().signOut().then(() => {
dispatch({ type: 'SIGNOUT_SUCCESS' })
});
}
}
ログイン用のと同様にfirebaseインスタンスを作ってログアウト用のメソッドを実行している.
そして非同期処理が完了次第,actionをdispatchしている.
次に,このactionを扱うreducerを定義する.ここもログイン用と同じだな.
const initState = {
authError: null
}
const authReducer = (state = initState, action) => {
switch(action.type) {
case 'LOGIN_ERROR':
console.log('login error');
return {
...state,
authError: 'Login failed'
}
case 'LOGIN_SUCCESS':
console.log('login success');
return {
...state,
authError: null
}
case 'SIGNOUT_SUCCESS':
console.log('signout success');
return state;
default:
return state;
}
}
export default authReducer
あとはLog OutをクリックしたらsignOutアクションクリエータをfire!してくれるようにします.
まずはアクションクリエータを孕んだdispatchをpropsとしてコンポネントに渡すためにmapDispatchToPropsをconnectしていく.
そしてLog Outのところをaタグに変更する.
import React from 'react'
import { NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import { signOut } from '../../store/actions/authActions'
const SignedInLinks = (props) => {
return (
<ul className="right">
<li><NavLink to='/create'>New Project</NavLink></li>
<li><a onClick={props.signOut}>Log Out</a></li>
<li><NavLink to='/' className="btn btn-floating pink lighten-1">NN</NavLink></li>
</ul>
)
}
const mapDispatchToProps = (dispatch) => {
return {
signOut: () => dispatch(signOut())
}
}
export default connect(null, mapDispatchToProps)(SignedInLinks);
次はログイン状況に応じてNavbar上のSignedInLinksとSignedOutLinksの表示切り替えを実装したい.
Authステータスのトラッキング
二つのコンポネントをラップしてるNavbarコンポネントをいじるしかないだろう.
ログインしているとき,state.firebase.authにはユーザーID示すuidが存在する.ログインしていないと存在しない.
これを利用して片方のコンポネントのみ表示させる.
import React from 'react'
import { Link } from 'react-router-dom'
import SignedInLinks from './SignedInLinks'
import SignedOutLinks from './SignedOutLinks'
import { connect } from 'react-redux'
const Navbar = (props) => {
const { auth } = props;
const links = auth.uid ? <SignedInLinks /> : <SignedOutLinks />
return (
<nav className="nav-wrapper grey darken-3">
<div className="container">
<Link to='/' className="brand-logo">MarioPlan</Link>
{ links }
</div>
</nav>
)
}
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth
}
}
export default connect(mapStateToProps)(Navbar);
次は今のと似たような条件分岐を使って軽微な調整をする.
Authの準備をまってやれ
Firebase Authenticationを使ったシステムはほぼ完成した.
しかし少し問題が生じている.
ブラウザでログインした状態でリロードすると,一瞬ではあるが,NavbarにSignedOutLinksが表示されてしまっている.
これはよくないUXの例だね.
Firebase Authenticationが初期化される前にコンポネントひいてはアプリ全体がレンダリングされてしまうと,ログインできていないステータスで表示される.さっきで言うところの,uidが存在していないからだ.
なので初期化が済んでログイン状況がわかるまでは,アプリがDOMにレンダリングされるのを阻止したい.
解決はとても簡単なのでサクッとやってしまおう.
src/index.jsをいじる.ここがアプリをレンダリングしている場所だ.
react-redux-firebaseストアエンハンサーの引数でattachAuthIsReadyをtrueにする.
これによってstoreのプロパティでfirebaseAuthIsReadyというメソッドが呼び出せるようになる.
これを呼び出すとfirebaseの初期化をまってくれて,そこにthenメソッドを使ってコールバック関数を呼び出し,その中でアプリをDOMにレンダリングする.
こうすれば,firebase初期化→アプリをDOMにレンダリングという順番が確定する.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from './store/reducers/rootReducer';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { reduxFirestore, getFirestore } from 'redux-firestore';
import { reactReduxFirebase, getFirebase } from 'react-redux-firebase';
import fbConfig from './config/fbConfig';
const store = createStore(rootReducer,
compose(
applyMiddleware(thunk.withExtraArgument({getFirebase, getFirestore})),
reduxFirestore(fbConfig),
reactReduxFirebase(fbConfig, {attachAuthIsReady: true})
)
);
store.firebaseAuthIsReady.then(() => {
ReactDOM.render(
<Provider store={store}><App /></Provider>, document.getElementById('root'));
serviceWorker.unregister();
})
ブラウザでリロードして修正できたのを確認してみよう.
ルートガード
今回のアプリでは,ユーザーがサイトを訪れた時(ログインしてない時)にはプロジェクト一覧とディティールを見せたくないし,新しいプロジェクトも作らせたくない.
なのでログインしていないユーザーが/や/create,/project/:idにアクセスしてもログインページ/signinにリダイレクトして欲しい.
それにはルートガードという手法をとる.
早速Dashboardからいじって行こう.
state.firebase.authでログイン状況が分かるんだったよね.こいつをコンポネントで使うためにmapStateToPropsにてpropsに渡す.
ログインしていないと!auth.uidがtrueとなり,react-router-domから提供されているRedirectで/signinへリダイレクトする.
import React, { Component } from 'react'
import Notification from './Notification'
import ProjectList from '../projects/ProjectList'
import { connect } from 'react-redux'
import { firestoreConnect } from 'react-redux-firebase'
import { compose } from 'redux'
import { Redirect } from 'react-router-dom'
class Dashboard extends Component {
render() {
const { projects, auth } = this.props;
if (!auth.uid) return <Redirect to='/signin' />
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<ProjectList projects={projects} />
</div>
<div className="col s12 m5 offset-m1">
<Notification />
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
console.log(state);
return {
projects: state.firestore.ordered.projects,
auth: state.firebase.auth
}
}
export default compose(
connect(mapStateToProps),
firestoreConnect([
{ collection: 'projects' }
])
)(Dashboard);
これで/用のルートガードができた.次は/create用のルートガードだ.ログインしてないからNew Projectボタンは押せないが/createと直接url入力すれば訪問できてしまっている.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createProject } from '../../store/actions/projectActions'
import { Redirect } from 'react-router-dom'
class CreateProject extends Component {
state = {
title: '',
content: ''
}
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault()
this.props.createProject(this.state)
}
render() {
const { auth } = this.props;
if (!auth.uid) return <Redirect to='/signin' />
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>
)
}
}
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth
}
}
const mapDispatchToProps = (dispatch) => {
return {
createProject: (project) => dispatch(createProject(project))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CreateProject)
同様のことをProjectDetailsでも実装する.
import React from 'react'
import { connect } from 'react-redux'
import { firestoreConnect } from 'react-redux-firebase'
import { compose } from 'redux'
import { Redirect } from 'react-router-dom'
const ProjectDetails = (props) => {
const { project, auth } = props;
if (!auth.uid) return <Redirect to='/signin' />
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
<div className="card-content">
<span className="card-title">{ project.title }</span>
<p>{ project.content }</p>
</div>
<div className="card-action gret lighten-4 grey-text">
<div>Posted by {project.authorFirstName} {project.authorLastName}</div>
<div>2nd, September, 2am</div>
</div>
</div>
</div>
)
} else {
return (
<div className="container center">
<p>Loaging project...</p>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
const id = ownProps.match.params.id;
const projects = state.firestore.data.projects;
const project = projects ? projects[id] : null
return {
project: project,
auth: state.firebase.auth
}
}
export default compose(
connect(mapStateToProps),
firestoreConnect([
{ collection: 'projects' }
])
)(ProjectDetails);
各自ブラウザでルートガードができているか確認して欲しい.
あとは逆にログインしているのに/signinや/signupにはアクセスして欲しくないのでそこもルートガードを実装する必要がある.
まずはSignInコンポネントからいじろうか.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { signIn } from '../../store/actions/authActions'
import { Redirect } from 'react-router-dom'
class SignIn extends Component {
state = {
email: '',
password: ''
}
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault()
this.props.signIn(this.state)
}
render() {
const { authError, auth } = this.props;
if (auth.uid) return <Redirect to='/' />
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 className="red-text center">
{ authError ? <p>{authError}</p> : null }
</div>
</div>
</form>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
authError: state.auth.authError,
auth: state.firebase.auth
}
}
const mapDispatchToProps = (dispatch) => {
return {
signIn: (creds) => dispatch(signIn(creds))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SignIn)
次はSignUpだ.
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import { connect } from 'react-redux'
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() {
const { auth } = this.props;
if (auth.uid) return <Redirect to='/' />
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>
)
}
}
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth
}
}
export default connect(mapStateToProps)(SignUp);
これで一通りのルートガードの実装が完了した.やったことを振り返るとAuthenticationのステータスを利用して条件に応じてRedirectさせることでコンテンツを保護した.
サインアップ機能
今はまだSIGN UPを押しても内部stateに保管された入力内容をconsole.log(state)するだけに留まっている.
本当はFirebase Authを使って新しいユーザーを作成したい.
どこに実装すればいい?非同期処理タスクを扱うんだからもちろんaction creatorだよね.
なのでauthActions.js内にサインアップ用のアクションクリエータを作って行こう.
export const signIn = (credentials) => {
return (dispatch, getState, {getFirebase}) => {
const firebase = getFirebase();
firebase.auth().signInWithEmailAndPassword(
credentials.email,
credentials.password
).then(() => {
dispatch({ type: 'LOGIN_SUCCESS' });
}).catch((err) => {
dispatch({ type: 'LOGIN_ERROR' });
});
}
}
export const signOut = () => {
return (dispatch, getState, {getFirebase}) => {
const firebase = getFirebase();
firebase.auth().signOut().then(() => {
dispatch({ type: 'SIGNOUT_SUCCESS' })
});
}
}
export const signUp = (newUser) => {
return (dispatch, getState, {getFirebase, getFirestore}) => {
const firebase = getFirebase();
const firestore = getFirestore();
firebase.auth().createUserWithEmailAndPassword(
newUser.email,
newUser.password
).then((resp) => {
return firestore.collection('users').doc(resp.user.uid).set({
firstName: newUser.firstName,
lastName: newUser.lastName,
initials: newUser.firstName[0] + newUser.lastName[0]
})
}).then(() => {
dispatch({ type: 'SIGNUP_SUCCESS' })
}).catch(err => {
dispatch({ type: 'SIGNUP_ERROR', err })
})
}
}
他のsignInとsignOutと違い,getFirestoreも使うのは理由がある.
Authenticationではメールと(自動生成の)IDだけしか情報を持たない.しかし名前などの追加情報も保管したいのでfirestoreでUsersコレクションを作成し,そこに個人データを保管する.
この時Authenticationで自動生成されたIDがUsersコレクションに保管されるデータのユニークIDとを一致させることで連携させるためだ.

newUserにはフォームに入力された情報が,respにはcreateUserWithEmailAndPasswordの返り値が入っている.
thenメソッドのコールバック関数内でusersコレクションにデータを追加している.usersコレクションは作成していないが,存在しないコレクション名を指定すると自動で作ってくれるから問題ない.
注意としてaddだと自動でデータに対してユニークなIDがつくが今回はそれを望まない.なぜならAuthenticationで自動生成されたIDと一致させたいからだ.
今回はdocを使用することで特定のIDを指定できるようにした.そこにsetメソッドで追加するデータを指定する.
あとはいつも通り成功時とエラー時のactionをdispatchしている.
次にこれらのactionを扱うreducerを設定していく.
const initState = {
authError: null
}
const authReducer = (state = initState, action) => {
switch(action.type) {
case 'LOGIN_ERROR':
console.log('login error');
return {
...state,
authError: 'Login failed'
}
case 'LOGIN_SUCCESS':
console.log('login success');
return {
...state,
authError: null
}
case 'SIGNOUT_SUCCESS':
console.log('signout success');
return state;
case 'SIGNUP_SUCCESS':
console.log('signup success');
return {
...state,
authError: null
}
case 'SIGNUP_ERROR':
console.log('signup failed');
return {
...state,
authError: action.err.message
}
default:
return state;
}
}
export default authReducer
action.type.messageには無効なメールアドレスだとかパスワードが短すぎるとかのエラーメッセージが含まれる.
これで呼び出される側の実装は終わった.あとは呼び出す側SignUpコンポネントでの実装のみだ.やっていこう.
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import { connect } from 'react-redux'
import { signUp } from '../../store/actions/authActions'
class SignUp extends Component {
state = {
email: '',
password: '',
firstName: '',
lastName: ''
}
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault();
this.props.signUp(this.state);
}
render() {
const { auth, authError } = this.props;
if (auth.uid) return <Redirect to='/' />
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 className="red-text center">
{ authError ? <p>{authError}</p> : null }
</div>
</div>
</form>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth,
authError: state.auth.authError
}
}
const mapDispatchToProps = (dispatch) => {
return {
signUp: (newUser) => dispatch(signUp(newUser))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SignUp);
それではブラウザでサインアップしてみよう.
password: 'test1234'

サインアップが無事完了してルートガードによってダッシュボード画面にきたらOK.
あとはfirebaseプロジェクトにユーザーが追加されているかも確認しよう.


AuthenticationのユーザーUIDとFirestoreのドキュメントのidが一致しているのが分かるだろう.
あとSignInと同様にエラーの時にauthErrorを使ってフォーム下部に赤字でエラーメッセージ表示するようにもしたので,わざと短いパスワードにしたりして動作確認してね.
ユーザープロフィールデータ
次はNavbarにログイン中に出てくるイニシャルが描かれたピンクの円をログインユーザーに合わせて設定したい.
今のところNavbarもSignedInLinksもユーザーのメールアドレスやIDの情報は持っているが,名前の情報は持っていない.
↓NavbarのmapStateToProps内にconsole.log(state)を設置してみた画像

じゃぁ名前の情報はどこにあるかと言ったらFirestoreのusersコレクションだった.
firebase.profileというプロパティがある.画像での状態はプロフィール情報を持っていないことを示す.ここに名前情報などを持ったオブジェクトをぶち込みたい.
実は方法はすごい簡単で,src/index.jsのreact-redux-firebaseに追加のプロパティを設定してあげればいい.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from './store/reducers/rootReducer';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { reduxFirestore, getFirestore } from 'redux-firestore';
import { reactReduxFirebase, getFirebase } from 'react-redux-firebase';
import fbConfig from './config/fbConfig';
const store = createStore(rootReducer,
compose(
applyMiddleware(thunk.withExtraArgument({getFirebase, getFirestore})),
reduxFirestore(fbConfig),
reactReduxFirebase(fbConfig, {useFirestoreForProfile: true, userProfile: 'users', attachAuthIsReady: true})
)
);
store.firebaseAuthIsReady.then(() => {
ReactDOM.render(
<Provider store={store}><App /></Provider>, document.getElementById('root'));
serviceWorker.unregister();
})
こいつらはreact-redux-firebaseとredux-firestore経由でユーザー情報を取得するための設定だ.
useFirestoreForProfileはfirebaseReducerにfirestoreデータベースとprofileオブジェクトを同期を可能にさせる設定だ.ただこのままではfirebaseReducerはどのコレクションと同期すればいいのか知らない.
なので,userProfileでusersとコレクション名を指定してあげた.
この状態でブラウザコンソールを確認してみよう.(他コンポネントのconsole.logとか残ってたら適宜削除してね)

見事firebase.profileにログインしているユーザーの情報がぶち込まれた.
これをもとにNavbarのピンク円を編集していく.
いつも通りmapStateToPropsでprofileをpropsに渡し,さらにコンポネント内でSignedInLinksにpropsとして渡している.
import React from 'react'
import { Link } from 'react-router-dom'
import SignedInLinks from './SignedInLinks'
import SignedOutLinks from './SignedOutLinks'
import { connect } from 'react-redux'
const Navbar = (props) => {
const { auth, profile } = props;
const links = auth.uid ? <SignedInLinks profile={profile} /> : <SignedOutLinks />
return (
<nav className="nav-wrapper grey darken-3">
<div className="container">
<Link to='/' className="brand-logo">MarioPlan</Link>
{ links }
</div>
</nav>
)
}
const mapStateToProps = (state) => {
console.log(state);
return {
auth: state.firebase.auth,
profile: state.firebase.profile
}
}
export default connect(mapStateToProps)(Navbar);
最後にSignedInLinksをいじって終わりだ.
import React from 'react'
import { NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import { signOut } from '../../store/actions/authActions'
const SignedInLinks = (props) => {
return (
<ul className="right">
<li><NavLink to='/create'>New Project</NavLink></li>
<li><a onClick={props.signOut}>Log Out</a></li>
<li><NavLink to='/' className="btn btn-floating pink lighten-1">
{props.profile.initials}
</NavLink></li>
</ul>
)
}
const mapDispatchToProps = (dispatch) => {
return {
signOut: () => dispatch(signOut())
}
}
export default connect(null, mapDispatchToProps)(SignedInLinks);
プロジェクト作成の細部
CreateProjectでプロジェクトを作成するとcreateprojectアクションクリエータが実行される.その内部ではfirestoreのprojectsコレクションにプロジェクトを追加している.
しかし,author~~の3つのプロパティは前にハードコーディングしたものをまだ使っている.これをログインしているユーザーの情報に対応させたいのでprojectActions.jsを修正していく.
getStateを使ってstateのデータにアクセスする.getState()はstate全体を返す.
export const createProject = (project) => {
return (dispatch, getState, { getFirebase, getFirestore }) => {
// make async call to database
const firestore = getFirestore();
const profile = getState().firebase.profile;
const authorId = getState().firebase.auth.uid;
firestore.collection('projects').add({
...project,
authorFirstName: profile.firstName,
authorLastName: profile.lastName,
authorId: authorId,
createdAt: new Date()
}).then(() => {
dispatch({ type: 'CREATE_PROJECT', project})
}).catch((err) => {
dispatch({ type: 'CREATE_PROJECT_ERROR', err })
})
}
};
これで適切な作成されるプロジェクトに適切なユーザー情報が記述されるはず.プロジェクトを作ってみましょう.



ダッシュボードにもちゃんと追加されたのを確認したのも束の間,問題に気づいただろうか.
Posted by ~~のところもハードコーディングだったので適切な表示ができていない.(ちなみにProjectDetailの方は前に修正したのでちゃんと適切な名前が表示されていると思う)
ProjectSummaryにて修正していこう.
import React from 'react'
const ProjectSummary = ({project}) => {
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 {project.authorFirstName} {project.authorLastName}</p>
<p className="grey-text">3rd September</p>
</div>
</div>
)
}
export default ProjectSummary;
次はプロジェクトを作成したらダッシュボード画面にリダイレクトするようにしよう.
CreateProjectをいじる.
App.jsで,react-router-domのRoteタグを使ってルート管理をしているわけだが,そのルーティングの対象のコンポネントは内部でpropsにreact-router-domの機能が入っていてその1つであるthis.props.history.push()が使える.
これにより画面の遷移が可能になる.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createProject } from '../../store/actions/projectActions'
import { Redirect } from 'react-router-dom'
class CreateProject extends Component {
state = {
title: '',
content: ''
}
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault()
this.props.createProject(this.state)
this.props.history.push('/')
}
render() {
const { auth } = this.props;
if (!auth.uid) return <Redirect to='/signin' />
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>
)
}
}
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth
}
}
const mapDispatchToProps = (dispatch) => {
return {
createProject: (project) => dispatch(createProject(project))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CreateProject)
適当にプロジェクトを作ってみてダッシュボード画面に遷移するかどうかと,ダッシュボードに今作ったプロジェクトは表示されているか確認しよう.
今回はここまで.
次回からは細かい調整と,いよいよCloud Functionに触れていく.そしてNotificationsも実装して完成まで持っていく.



