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

1/7/2020 ReactReduxFirebase

前回ではReactでアプリの見た目と簡単なページ遷移を設定した.

今回は一気にRedux, Thunk, Firebaseを使っていく.

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

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

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

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

Reduxを使ってReducer実装

まずはreduxとReactとReduxの接着剤的な役割のreact-reduxをインストールする.

cd marioplan
npm install redux react-redux@5.1.1
1
2

インストールが完了したら.src/index.jsに移動してstoreを作成する.Reduxの基礎がわかってる人なら大丈夫だと思うがstoreはアプリ全体の状態を管理するためのものだ.React単体ではコンポネント単位でしか状態を保管できなかった.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore } from 'redux'

const store = createStore();

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

createStoreの引数にはreducerを与えないといけない.reducerがどんなものか忘れてしまった人は たぶんこれが一番分かりやすいと思います React + Redux のフロー図解 (opens new window) で確認してみて欲しい.

reducerは一般的にたくさんの種類のactionを扱うので1つにまとめると煩雑になってしまいがちだ.なので複数に分けて作成しルートリデューサーに統合してstoreに飲み込ませる.

それではreducerを1つ1つ作っていく.srcフォルダ内にstoreを作成し,さらにその中にreducersを作成. そこにauthReducer.jsを設置し書き込んでいく.

const initState = {}
const authReducer = (state = initState, action) => {
    return state
}

export default authReducer
1
2
3
4
5
6

reducerはアプリがスタートした時に初めて実行されるが,最初はstateがアクティブでないので初期値をデフォルト値として与える必要がある.関数内はというと今はまだstateをreturnするだけにしておく.

他のreducerも作っていく.

const initState = ()
const projectReducer = (state = initState, action) => {
    return state
}

export default projectReducer
1
2
3
4
5
6

これらをまとめるルートリデューサーを作っていく.

import authReducer from './authReducer'
import projectReducer from './projectReducer'
import { combineReducers } from 'redux'

const rootReducer = combineReducers({
    auth: authReducer,
    project: projectReducer
})

export default rootReducer
1
2
3
4
5
6
7
8
9
10

combineReducerにはオブジェクトを渡すが,この時プロパティがそのままstateのプロパティに追加され,それらはreducerと対応する. つまりauthReducerstateauthプロパティを更新,projectReducerprojectプロパティを更新する役割を持っている.

ルートリデューサーができたのでindex.jsに戻ってcreateStoreに読み込ませる. そしてアプリがstoreにアクセスできるようにProviderにstoreを渡す形でAppを囲う.Providerreact-reduxから提供されていて先ほど説明した通りまさにReactとRedux(store)を繋ぐ接着剤の役割を担っている.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore } from 'redux'
import rootReducer from './store/reducers/rootReducer'
import { Provider } from 'react-redux'

const store = createStore(rootReducer);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>
    , document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

ダミーデータを置いてreduxの振舞いの確認

ここではprojectReducerにでもダミーデータを置いてProjectListひいてはProjectSummarypropsとしてダミーデータを渡してうまく表示させてみたいと思う.

const initState = {
    projects: [
        {id: '1', title: 'help me find peach', content: 'blah blah blah'},
        {id: '2', title: 'collect all the stars', content: 'blah blah blah'},
        {id: '3', title: 'egg hunt with yoshi', content: 'blah blah blah'},
    ]
}
const projectReducer = (state = initState, action) => {
    return state
}

export default projectReducer
1
2
3
4
5
6
7
8
9
10
11
12

initStateprojectsがプロパティのオブジェクト配列を設定.これでprojectReducerはこのダミーデータなstateをreturnする.このreturnされたオブジェクトはrootReducerで定義した通り,state中のprojectプロパティにて管理される.

それではDashboardでダミーデータにアクセスして下位のコンポネントに渡して行こう.

import React, { Component } from 'react'
import Notification from './Notification'
import ProjectList from '../projects/ProjectList'
import { connect } from 'react-redux'

class Dashboard extends Component {
    render() {
        const { projects } = this.props

        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) => {
    return {
        projects: state.project.projects
    }
}

export default connect(mapStateToProps)(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

各コンポネントをstoreとつなげるにはconnectを使う.connectは関数でありreact-reduxから提供されている.Providerといい本当に優秀な接着剤である.

connectにはmapStateToPropsmapDispatchToPropsという2つの関数を渡せる.今は前者だけを渡す.

  • mapStateToProps・・・コンポネントで使いたいstorestateを宣言することでpropsとしてコンポネント内で使えるようにする関数
  • mapDispatchToProps・・・action creatorを使う場合に予めdispatchに噛ませた状態をpropsとしてコンポネント内で使えるようにする関数
const { projects } = this.props
1

で吸収して

<ProjectList projects={projects} />
1

でうまくProjectListpropsとしてダミーデータを渡している.

import React from 'react'
import ProjectSummary from './ProjectSummary'

const ProjectList = ({projects}) => {
    return (
        <div className="project-list section">
            { projects && projects.map(project => {
                return (
                    <ProjectSummary project={project} key={project.id} />
                )
            })}
        </div>
    )
}

export default ProjectList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

同様にpropsとしてprojectsを受け取る. プロジェクトが増えてくることを考えると,ProjectSummaryをひたすら羅列するのは非常に面倒なのでこれを機にmapを使って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 Net Ninja</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

タイトルに使用してみる.この状態でnpm startしてみる. スクリーンショット 2019-10-24 21.28.59.png タイトルがダミーデータと一致していれば成功だ.

Reduxで非同期処理を扱う

ダミーデータではなく本物のデータベースからデータを取ってくる時にどこにコードを書けばいいだろうか?

reducer内か?いや,それだとうまくワークしない.データベースへのアクセスには幾分か時間がかかる.その間にreturn stateしてしまうからだ. では,コンポネント自身か?レンダリングのタイミングでデータベースから取ってきてactionとしてreducerに取り込んでstateを更新できる.しかしこれもまたアクセス時間やらが邪魔して細かな懸念が拭い切れない. やはりコンポネントの外でデータ,state操作はやりたい.

ここでRedcuxライフサイクルの概念図をみてみよう. スクリーンショット 2019-10-24 22.08.19.png

中核となるstoreがあって,stateからマッピングしてcomponentpropsDOMとして組み込める. もしstateを変更したいなら,componentからactiondispatchする.actionにはreducerが判断するためのtypeと具体的に何がしたいかのpayloadを含める.んでactionreducerに渡され,そこでstateが更新され,変更点がレンダリングされコンポネントのビューに反映される.

これが基本のライフサイクルである.そして僕らが考えている「どうやって外部からデータを取ってくるか」だが, まぁもうわかってると思うが,DISPATCH ACTIONREDUCERの間である. actiondispatchされたら(正確にはまだdispatchされない),一旦止めて非同期処理を実行する.その後,止めていたdispatchを再開し,reducerを実行するという流れだ.

これを可能にするためにredux-thunkと呼ばれるmiddlewareを使用する. middlewareがさっきの2点間でコードを実行する.このおかげでaction creatorの中で非同期処理が行える. スクリーンショット 2019-10-24 22.29.53.pngactiondispatchする」から「actionを孕んだaction creatordispatchする」というイメージだ. redux thunkactionではなく関数を返す. 関数の中では,dispatchを一旦中止し,非同期処理を行いdispatchを再開する.

超要約すると,dispatchactionの代わりにaction creator渡すと,中で非同期処理とかしてそのレスポンスも含めたactionreducerに渡してくれるよ.ということ.

解説を省いたけどaction creatorがあまり分からないという人は 今から始めるReact入門 〜 Redux 編: Redux 単体で状態管理をしっかり理解する (opens new window) を参照して欲しい.

Thunkのセットアップ

まずThunkのインストール

npm install redux-thunk
1

src/index.jsredux-thunkなるmiddlewarestoreに読み込む.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './store/reducers/rootReducer'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'

const store = createStore(rootReducer, applyMiddleware(thunk));

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>
    , document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
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

reduxからストアエンハンサーapplyMiddlewareという名前の通りmiddlewarestoreに適用するための関数もインポートしている.引数には複数のmiddlewareを指定することが可能である.ストアエンハンサーはいくつか存在するがどれもstoreに機能性を提供する.今回の場合はaction creator内で関数を返すことができるようになるという機能性が提供されているわけだ.

それではシンプルなaction creatorを作っていく.storeactionsフォルダを作成. そこにprojectActions.jsを設置.これプロジェクトに関するaction creatorを持つファイルである.

export const createProject = (project) => {
    return (dispatch, getState) => {
        // make async call to database
        dispatch({ type: 'CREATE_PROJECT', project})
    }
};
1
2
3
4
5
6

action creatorが返す関数の引数にはactionreducerに送る関数dispatchstateを読み込むためのgetStateが渡されている.

次にやることはCreateProjectコンポネント内でこのaction creatordispatchすることだ.

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createProject } from '../../store/actions/projectActions'

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() {
        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 mapDispatchToProps = (dispatch) => {
    return {
        createProject: (project) => dispatch(createProject(project))
    }
}

export default connect(null, 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

mapDispatchToPropsaction creatorを孕んだdispatchcreateProjectとしてpropsに渡し,handleSubmit内で使った.

次にactionを受け取るprojectReducerを書いていく.

const initState = {
    projects: [
        {id: '1', title: 'help me find peach', content: 'blah blah blah'},
        {id: '2', title: 'collect all the stars', content: 'blah blah blah'},
        {id: '3', title: 'egg hunt with yoshi', content: 'blah blah blah'},
    ]
}
const projectReducer = (state = initState, action) => {
    switch (action.type) {
        case 'CREATE_PROJECT':
            console.log('created project', action.project)
    }
    return state
}

export default projectReducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

今はとりあえず新しいプロジェクトを作ったら,コンソールに内容が表示するようにした. スクリーンショット 2019-10-24 23.34.45.png 本当はCREATE押したらFire storeにデータをaddして欲しい.

ということでいよいよFirebaseを扱っていこうと思う.

Firebaseセットアップ

firebase (opens new window)に飛んで適当にログインしてコンソールへ移動.

  • 1.プロジェクトを追加を押す
    1. 適当にプロジェクト名を記入
    1. Googleアナリティクスは今回は使わない.
    1. プロジェクト作成完了

ここからfirebaseとアプリをつなげる作業. プロジェクトのダッシュボードにてHTMLタグのようなマークをクリック.

    1. アプリの適当なニックネームを記入.Hosting機能はあとで別途設定するのでチェックを外す.
    1. Firebase SDKの追加でコードがババっと出てくるが以下の情報だけをコピー.上の方のはnpmで直接インストールするからいらない.

注意: 下コードはapiKeyしかり諸々が筆者固有のユニークなものなので皆さんにはご自身のブラウザで提示されたコードをコピーして欲しい.

// Your web app's Firebase configuration
var firebaseConfig = {
    apiKey: "AIzaSyCZItfqmvg5jjK0JXR9cnoW9xcHmPLWkfs",
    authDomain: "net-ninja-marioplan-178f9.firebaseapp.com",
    databaseURL: "https://net-ninja-marioplan-178f9.firebaseio.com",
    projectId: "net-ninja-marioplan-178f9",
    storageBucket: "net-ninja-marioplan-178f9.appspot.com",
    messagingSenderId: "240847046405",
    appId: "1:240847046405:web:45a4511e8e31622089196c",
    measurementId: "G-BJYQ8GHCBL"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
1
2
3
4
5
6
7
8
9
10
11
12
13

firebaseライブラリをインストール

npm install firebase
1

firebaseセットアップのためのconfigファイルを作成する.srcconfigフォルダを作成. そこにfbConfig.jsを作成し先ほどコピーした内容をそのまま貼り付けて,最初と最後に少し足す. firebaseのfirestoreauthを使えるようにインポートする.最後から2行目はfirestoreの初期化. これはfirebaseライブラリの更新であり,firebaseのタイムスタンプの動作を変更する.これによりあとでコンソールがエラーを吐くのを避けている.

apiKeyなどは他人に見られても大丈夫なのでご安心を.最後の方でfirebase側でセキュリティのルールについても設定する.

import firebase from 'firebase/app'
import 'firebase/firestore'
import 'firebase/auth'

// Your web app's Firebase configuration
var firebaseConfig = {
    apiKey: "AIzaSyCZItfqmvg5jjK0JXR9cnoW9xcHmPLWkfs",
    authDomain: "net-ninja-marioplan-178f9.firebaseapp.com",
    databaseURL: "https://net-ninja-marioplan-178f9.firebaseio.com",
    projectId: "net-ninja-marioplan-178f9",
    storageBucket: "net-ninja-marioplan-178f9.appspot.com",
    messagingSenderId: "240847046405",
    appId: "1:240847046405:web:45a4511e8e31622089196c",
    measurementId: "G-BJYQ8GHCBL"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

firebase.firestore()

export default firebase;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

最後にエクスポートすることでコンポネントでインポートすれば,firebaseとやりとりができるようになる.

Firestoreのデータとコレクション

firestoreはNoSQLと呼ばれるデータベース.firestoreではデータの集まりをコレクションという. 今回のでいうとプロジェクト単体がデータとなりその集合をprojectsというコレクションで管理する.他にもusersnotificationsというコレクションを作成する. スクリーンショット 2019-10-25 16.11.10.png コレクション内のデータはJavaScriptオブジェクトのような形式をとる.

まずはprojectsコレクションを作成していく.そこにダミーデータを設置し,アプリからアクセスできるようにして行こう.

firebaseプロジェクトのDatabase画面にいき,データベースの作成を押す.

ロックモードかテストモードかを選ばされると思いますが,データへのアクセスやデータの更新を簡単にするためにテストモードを選びます.セキュリティのルールを最後の方で書き換えるので大丈夫.

ロケーション(多分サーバーの場所)はasia-northeast1が東京なのでそれを選んで完了をクリック.

コレクションIDがprojectsなコレクションを作る.

その際の以下のダミープロジェクトを1つ作る. スクリーンショット 2019-10-25 16.29.27.png このプロジェクトにはユニークなIDが振り分けられる. timeStampプロパティも付けたいのだがそれはあとで実装する.Fieldはあとでも追加できるので.

ReduxとFirebaseを繋ぐ

まずはCreateProjectコンポネントにおいてフォームを入力したら入力内容(プロジェクト)がFirestorに追加されることを目指す. そのためにはprojectAction.jsアクションクリエータでfirebaseとの非同期処理を書く必要がある.

ここでの実装方法として単純にfirebaseライブラリをインストールしてもいいのだが,reduxとfirebaseの共生のためにデザインされたいくつかのパッケージを代わりにインストールしたいと思う.こいつらの名前をreact-redux-firebaseredux-firestoreという.豪華な名前だ.

react-redux-firebaseはfirebaseサービス全体をバインディングする. redux-firestoreはreduxとfirestore databaseとをバインディングする.

この2つのパッケージをインストールすることで,firebaseのAPIを使用してデータベースと通信できる.action creatorの中でね. つまり,firestoreデータベースとstoreを同期することができる.

// ここ最近でreact-redux-firebaseが大きなアップデートをしたようで最新のversionだとエラーを吐くので安全なバージョンを選択.
npm install react-redux-firebase@2.4.1
npm install redux-firestore@0.9.0
1
2
3

インストールが完了したら,action creatorでfirebaseまた,firestoreAPIにアクセスできるようにsrc/index.jsにて読み込む.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './store/reducers/rootReducer'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import { getFirestore } from 'redux-firestore'
import { getFirebase } from 'react-redux-firebase'

const store = createStore(rootReducer, applyMiddleware(thunk.withExtraArgument({ getFirebase, getFirestore })));

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>
    , document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
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

withExtraArgumentの引数にインポートしたものを渡すことで,渡したものがredux-thunkから呼び出せる.つまりaction creatorで渡したものを使える.つまり,action creator内でfirebaseとの非同期処理が行える.

早速projectActionsをいじる.

export const createProject = (project) => {
    return (dispatch, getState, { getFirebase, getFirestore }) => {
        // make async call to database
        dispatch({ type: 'CREATE_PROJECT', project})
    }
};
1
2
3
4
5
6

だがこのままでは2つのget~~は機能しない.なぜなら,こいつらは僕らのfirebaseプロジェクトを知らない. なので先ほど作ったfirebaseプロジェクトの情報を持ったfbConfigファイルを2つのパッケージに教えてやらないといけない.

そのためにはストアエンハンサーが必要だ. ストアエンハンサーについては前に少し書いたが,index.jsでいうと,thunkがミドルウェアでapplyMiddlewareがストアエンハンサーである.

つまりstoreに複数のストアエンハンサーを設定することになるのだが,そのためにはreduxからcomposeなるものを使ってまとめる必要がある. 複数のreducercombineReducersを使ってまとめたように.

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)
    )
);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>
    , document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
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
27
28
29
30
31
32

firebase用の2つのパッケージからreduxFirestore, reactReduxFirebaseというストアエンハンサーをインポートしている. そしてfbConfigを渡すことでprojectActionsに渡されたgetFirebasegetFirestoreが僕らのfirebasプロジェクトを認識してくれる.

#Firestoreにデータを追加してみる firebasとreduxの接続設定が完了したので,実際に非同期処理を実装してみよう.

export const createProject = (project) => {
    return (dispatch, getState, { getFirebase, getFirestore }) => {
        // make async call to database
        const firestore = getFirestore();
        firestore.collection('projects').add({
            ...project,
            authorFirstName: 'Net',
            authorLastName: 'Ninja',
            authorId: 12345,
            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

まずgetFirestoreの初期化することでfirestoreインスタンスが手に入る. firestore.collection('projects').addのところはもう言わずもがな,projectsコレクションにaddの引数のオブジェクトを追加する命令.

この命令は非同期で少なからず時間がかかる.そして終わるまではdispatchするのを待って欲しいので,thenを使う.またエラーが起きた用のcatchでエラー用のアクションをdispatchするようにもした.

次にreducerをいじる.

const initState = {
    projects: [
        {id: '1', title: 'help me find peach', content: 'blah blah blah'},
        {id: '2', title: 'collect all the stars', content: 'blah blah blah'},
        {id: '3', title: 'egg hunt with yoshi', content: 'blah blah blah'},
    ]
}
const projectReducer = (state = initState, action) => {
    switch (action.type) {
        case 'CREATE_PROJECT':
            console.log('created project', action.project);
            return state;
        case 'CREATE_PROJECT_ERROR':
            console.log('create project error', action.err);
            return state;        
        default: 
            return state;
    }
}

export default projectReducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

とりあえずコンソール出力してreturn stateするだけ.

firestoreとつながったか試しにプロジェクトを作ってみよう. スクリーンショット 2019-10-25 21.00.34.png

コンソールされたのを確認してfirestoreへいくと. スクリーンショット 2019-10-25 21.01.43.png 無事firestoreにデータを追加することに成功した.

しかしアプリのダッシュボードには依然としてダミーデータしか載ってない.次はダミーデータでなくfirestoreのprojectsコレクションのデータを取得して表示するようにしたい.

Firestoreデータとの同期

そのためにはreduxのstateとfirestoreデータを同期する必要がある.そのための必要なパッケージは先ほどインストールしたredux-firestoreである. どこで使うのかというとrootReducer.jsである. redux-firestoreから予め定義されたfirestoreReducerというリデューサーをインポートしてcombineReducersしてやるのだ.

こいつはもう僕らのfirebaseプロジェクトを知っているから,バックグランドでfirestoreデータベースとreduxのstateとを同期してくれる. 僕らがしないといけないのはcombineReducersで固有のプロパティを与えてやることくらいだ.

import authReducer from './authReducer'
import projectReducer from './projectReducer'
import { combineReducers } from 'redux'
import { firestoreReducer } from 'redux-firestore'

const rootReducer = combineReducers({
    auth: authReducer,
    project: projectReducer,
    firestore: firestoreReducer
})

export default rootReducer
1
2
3
4
5
6
7
8
9
10
11
12

これでstateのfirestoreプロパティにfirestoreデータベースが自動的に同期されるようになった.

同期するデータはその瞬間にどのコンポネントがアクティブなのかということに依存する.つまりどんなデータをコンポネントが欲するかに依存する. そして所望されたデータがfirestoreReducerによって同期されるというわけだ.

なので今後僕らが同期のためにやるのはコンポネントからfirestoreReducerに対してどんなデータが欲しいかを伝えることだけだ.

Dashboardでダミーデータの代わりにfirestoreのデータを表示するようにいじって行こう.

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'

class Dashboard extends Component {
    render() {
        const { projects } = this.props

        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
    }
}

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

コンポネントとfirestoreをつなげるためにreact-redux-firebaseからfirestoreConnectをインポートする.

これをハイオーダーコンポネント(以下HOC)としてDashboardと繋げたいのだが,connect(mapStateToProps)という別のHOCを使用している.2つのHOCを使用するためにはストアエンハンサーの統合に使ったcomposeをここでも使う.

ちなみにfirestoreConnectはオブジェクト配列を受け取る.このオブジェクトではどのコレクションと同期したいかを宣言している.これでrootReducerで宣言した通りstateのfirestoreに所望したデータが保管される.

コンポネントをロードしたとき,firestoreデータが更新された時にfirestoreReducerを誘発しprojectsコレクションとstore stateを同期する.

またmapStateToPropsprojectsに渡す値をダミーデータでなくfirestoreのデータに変更している.

結果こうなる. スクリーンショット 2019-10-25 22.26.15.png

プロジェクトディティールへの連奏

次はダッシュボードのプロジェクトをクリックしたらディティール画面へ遷移するようにしたい. プロジェクトをマッピングしているのはProjectListコンポネントなのでそこでProjectSummaryコンポネントをLinkタグで囲めばいい.

import React from 'react'
import ProjectSummary from './ProjectSummary'
import { Link } from 'react-router-dom'

const ProjectList = ({projects}) => {
    return (
        <div className="project-list section">
            { projects && projects.map(project => {
                return (
                    <Link to={'/project/' + project.id} key={project.id}>
                        <ProjectSummary project={project} />
                    </Link>
                )
            })}
        </div>
    )
}

export default ProjectList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
スクリーンショット 2019-10-25 22.49.14.png

なんとか遷移は成功した.次にProjectDetailsコンポネントを整形していく.

firestoreのデータを使うためにDashboardでしたようにHOCをcomposeする. mapStateToProps部分だが,idにurlのid部分を代入し,それとprojectsを元に今回表示したいプロジェクトを特定しpropsとしてコンポネントに渡している.

コンポネントではprojectがローディング中にelseの方が実行され,ロード完了次第,ifがtrueの方が実行される. なので画面をリロードすると一瞬elseの文が表示される.

import React from 'react'
import { connect } from 'react-redux'
import { firestoreConnect } from 'react-redux-firebase'
import { compose } from 'redux'

const ProjectDetails = (props) => {
    const { project } = props;
    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
    }
}

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
スクリーンショット 2019-10-25 23.02.24.png

今回はここまで.

次回からはFirebase Authを使ってログイン機能の実装をしていく.

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