前回では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
も実装して完成まで持っていく.