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

1/8/2020 ReactReduxFirebase

前回では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による自動生成)などが保管できる.しかし自由度は低く,名前や,パスワードなどのカスタムプロパティは保存できない.

なのでそれらカスタムプロパティはfirestoreUsersコレクションを作成し,そこに保存することにする. そこにAuthenticationで作成したIDでもってデータを保管すればAuthentication内のデータとの結びつけもできることになる. スクリーンショット 2019-10-26 14.08.45.png

まずはFirebase Authenticationを利用可能にしてみよう. firebaseのダッシュボードからAuthenticationページに行って,ログイン方法を設定をクリック,

FacebookやTwitterなどたくさんのログインメソッドが出てくる.今回は単純にメール/パスワードを有効にする.(メールリンクは無効のままで)

ユーザータブにとんで,テストユーザーを作ってみよう. スクリーンショット 2019-10-26 14.17.18.png

こいつはまだ追加の情報を持ってないし,firestoreで連携していない. 最終的にはUsersコレクションを作って,ユーザと連携するようにしたい.

ReduxとFirebase Authの接続

Firebase Authの設定が完了したので次はアプリ内で使えるようにしたい.具体的にはユーザーのAuthenticationステータスを常に追っている状態にしたい.ログインしたのか,ログアウトしたのか.というようなステータスを.

そのためにはfirebaseからFirebase Authenticationのステータスをreduxのstoreに同期してstateからステータスを確認できるようにする必要がある.

前回,firestoreReducerを使って,firestorestateを同期した.今回も似たようなことをすればいい.

ということでrootReducerfirebaseReducerを追加する.

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

firebaseReducerによってfirebaseの全てのステータスがreduxひいてはstateに同期される.stateのfirebaseオブジェクトにね.

これでセットアップは完了した.では実際にコンポネントからAuthenticationステータスにアクセスしてみよう.

今回はNavbarコンポネントでアクセスしていく.ここではSignedInLinksSignedOutLinksコンポネントが呼び出されているが,①で書いたように本来はログイン状況に応じて片方のみを表示したいのだ.

まずはコンポネントを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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

実行してコンソールから確認してみると, スクリーンショット 2019-10-26 14.54.24.png firebaseauthプロパティをみるとisEmptytrueになっている.これはプロフィールが存在しないことを表している.つまりログインしていないことを表す.

次はログイン機能を実装していく.

ログイン機能

メールアドレスとパスワードを使ってログインするにはfirebaseプロジェクトとの非同期処理を行う必要がある.

それはどこでやればいいか.もうわかるよね.action creatorだ. ということでサインインに関するaction creatorを作成していく.

おさらいだけどthunkのおかげでdispatchを一旦止めて代わりに関数を返すことができたよね.そこでdispatchやfirebaseに関するインスタンスを作るgetFirebaseを受け取って非同期処理が行えた.

今回はauth関数,そしてsignInWithEmailAndPasswordメソッドを使って引数に必要な情報を渡すことでログインできる.

そしてprojectActions.js同様,非同期処理には幾分か時間がかかることを考慮してthenメソッドを使ってコールバック関数を呼び出し,その中でactiondispatchする.

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' });
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

credentialsにはメールアドレスやパスワードが含まれる.

次にこれらのactionを扱うreducerauthReducer.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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注意としてrootReducerを見ればわかるがここでのstateはreduxストアのstate.authに対応する.state.firebaseではなくて.

あとはSignInコンポネントからauthActionsアクションクリエータをdispatchすればいい. そのためにはauthActionsを孕んだdisaptchpropsとしてコンポネントに渡す.方法はもちろん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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

エラーがあったとき用にmapStateToPropsauthErrorを渡してフォーム下部でログイン失敗するとauthErrorが表示されるようにした. スクリーンショット 2019-10-26 15.58.44.png わざとログイン失敗してauthErrorが表示されるのも確認してみて欲しい.

次はログアウトを実装したいと思う.

ログアウト機能

ログアウト用のコンポネントは作ってなかったよね. SignedInkLinksLog 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' })
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

ログイン用のと同様にfirebaseインスタンスを作ってログアウト用のメソッドを実行している. そして非同期処理が完了次第,actiondispatchしている.

次に,この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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

あとはLog OutをクリックしたらsignOutアクションクリエータをfire!してくれるようにします.

まずはアクションクリエータを孕んだdispatchpropsとしてコンポネントに渡すためにmapDispatchToPropsconnectしていく.

そして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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

ログインしてからログアウトして,ブラウザで確認してみよう. スクリーンショット 2019-10-26 16.43.44.png

次はログイン状況に応じてNavbar上のSignedInLinksSignedOutLinksの表示切り替えを実装したい.

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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

ログインしたりログアウトしたりしてブラウザで確認しよう. スクリーンショット 2019-10-26 17.00.12.png スクリーンショット 2019-10-26 17.00.54.png

次は今のと似たような条件分岐を使って軽微な調整をする.

Authの準備をまってやれ

Firebase Authenticationを使ったシステムはほぼ完成した. しかし少し問題が生じている.

ブラウザでログインした状態でリロードすると,一瞬ではあるが,NavbarSignedOutLinksが表示されてしまっている. これはよくないUXの例だね.

Firebase Authenticationが初期化される前にコンポネントひいてはアプリ全体がレンダリングされてしまうと,ログインできていないステータスで表示される.さっきで言うところの,uidが存在していないからだ.

なので初期化が済んでログイン状況がわかるまでは,アプリがDOMにレンダリングされるのを阻止したい.

解決はとても簡単なのでサクッとやってしまおう.

src/index.jsをいじる.ここがアプリをレンダリングしている場所だ.

react-redux-firebaseストアエンハンサーの引数でattachAuthIsReadytrueにする. これによって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();
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

ブラウザでリロードして修正できたのを確認してみよう.

ルートガード

今回のアプリでは,ユーザーがサイトを訪れた時(ログインしてない時)にはプロジェクト一覧とディティールを見せたくないし,新しいプロジェクトも作らせたくない. なのでログインしていないユーザーが//create/project/:idにアクセスしてもログインページ/signinにリダイレクトして欲しい. それにはルートガードという手法をとる.

早速Dashboardからいじって行こう.

state.firebase.authでログイン状況が分かるんだったよね.こいつをコンポネントで使うためにmapStateToPropsにてpropsに渡す.

ログインしていないと!auth.uidtrueとなり,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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

これで/用のルートガードができた.次は/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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

同様のことを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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

各自ブラウザでルートガードができているか確認して欲しい.

あとは逆にログインしているのに/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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

次は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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

これで一通りのルートガードの実装が完了した.やったことを振り返ると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 })
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

他のsignInsignOutと違い,getFirestoreも使うのは理由がある. Authenticationではメールと(自動生成の)IDだけしか情報を持たない.しかし名前などの追加情報も保管したいのでfirestoreUsersコレクションを作成し,そこに個人データを保管する. この時Authenticationで自動生成されたIDUsersコレクションに保管されるデータのユニークIDとを一致させることで連携させるためだ. スクリーンショット 2019-10-26 14.08.45.png

newUserにはフォームに入力された情報が,respにはcreateUserWithEmailAndPasswordの返り値が入っている. thenメソッドのコールバック関数内でusersコレクションにデータを追加している.usersコレクションは作成していないが,存在しないコレクション名を指定すると自動で作ってくれるから問題ない.

注意としてaddだと自動でデータに対してユニークなIDがつくが今回はそれを望まない.なぜならAuthenticationで自動生成されたIDと一致させたいからだ.

今回はdocを使用することで特定のIDを指定できるようにした.そこにsetメソッドで追加するデータを指定する. あとはいつも通り成功時とエラー時のactiondispatchしている.

次にこれらの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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

それではブラウザでサインアップしてみよう. password: 'test1234' スクリーンショット 2019-10-26 22.29.55.png

サインアップが無事完了してルートガードによってダッシュボード画面にきたらOK.

あとはfirebaseプロジェクトにユーザーが追加されているかも確認しよう. スクリーンショット 2019-10-26 22.33.10.png スクリーンショット 2019-10-26 22.33.43.png AuthenticationユーザーUIDFirestoreのドキュメントのidが一致しているのが分かるだろう.

あとSignInと同様にエラーの時にauthErrorを使ってフォーム下部に赤字でエラーメッセージ表示するようにもしたので,わざと短いパスワードにしたりして動作確認してね.

#ユーザープロフィールデータ 次はNavbarにログイン中に出てくるイニシャルが描かれたピンクの円をログインユーザーに合わせて設定したい.

今のところNavbarSignedInLinksもユーザーのメールアドレスやIDの情報は持っているが,名前の情報は持っていない.

NavbarmapStateToProps内にconsole.log(state)を設置してみた画像 スクリーンショット 2019-10-26 23.09.30.png じゃぁ名前の情報はどこにあるかと言ったらFirestoreusersコレクションだった. firebase.profileというプロパティがある.画像での状態はプロフィール情報を持っていないことを示す.ここに名前情報などを持ったオブジェクトをぶち込みたい.

実は方法はすごい簡単で,src/index.jsreact-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();
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

こいつらはreact-redux-firebaseredux-firestore経由でユーザー情報を取得するための設定だ.

useFirestoreForProfilefirebaseReducerfirestoreデータベースとprofileオブジェクトを同期を可能にさせる設定だ.ただこのままではfirebaseReducerはどのコレクションと同期すればいいのか知らない. なので,userProfileusersとコレクション名を指定してあげた.

この状態でブラウザコンソールを確認してみよう.(他コンポネントのconsole.logとか残ってたら適宜削除してね) スクリーンショット 2019-10-26 23.34.32.png 見事firebase.profileにログインしているユーザーの情報がぶち込まれた. これをもとにNavbarのピンク円を編集していく.

いつも通りmapStateToPropsprofilepropsに渡し,さらにコンポネント内でSignedInLinkspropsとして渡している.

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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

最後に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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

プロジェクト作成の細部

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 })
        })
        
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

これで適切な作成されるプロジェクトに適切なユーザー情報が記述されるはず.プロジェクトを作ってみましょう. スクリーンショット 2019-10-27 0.06.17.png スクリーンショット 2019-10-27 0.07.56.png スクリーンショット 2019-10-27 0.09.42.png ダッシュボードにもちゃんと追加されたのを確認したのも束の間,問題に気づいただろうか. 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
スクリーンショット 2019-10-27 0.17.41.png

次はプロジェクトを作成したらダッシュボード画面にリダイレクトするようにしよう.

CreateProjectをいじる. App.jsで,react-router-domRoteタグを使ってルート管理をしているわけだが,そのルーティングの対象のコンポネントは内部でpropsreact-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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

適当にプロジェクトを作ってみてダッシュボード画面に遷移するかどうかと,ダッシュボードに今作ったプロジェクトは表示されているか確認しよう.

今回はここまで.

次回からは細かい調整と,いよいよCloud Functionに触れていく.そしてNotificationsも実装して完成まで持っていく.

Last Updated: 4/11/2021, 1:24:08 PM