LoginSignup
11
9

More than 5 years have passed since last update.

react, redux, react-router, pouchdbによる超簡易版ever note - PouchDB APIの非同期コール,pouch.on('change')による同期,react-routerとの統合

Last updated at Posted at 2016-03-01

react, redux, react-router, pouchdbの練習として作った超簡易サンプルアプリです.

  • 保存したメモのタイトル一覧が見れる
  • 一覧画面でタイトルをクリックすると詳細画面に飛ぶ
  • メモは複数クライアント間で同期される (via PouchDB)

というような感じです

pouchdbの内容をreduxのstoreに同期させるpouch-redux-middlewareという便利なパッケージもありますが,今回はこれは使わず直接PouchDBのAPIを叩いています.これは,

  • 扱うDBが大きくなってくると,リモートのDBの内容を全てクライアントのstateに保持するのは非効率
  • attachmentsを扱うときは直接PouchDBのAPIを触りたい

という理由からです.
今回のサンプルアプリではDBのサイズはそんなに大きくならないし,attachmentsも扱わないので,これらのメリットはありませんが,いずれ大きなアプリを作る際の練習ということで.

リポジトリはこちらになります:
https://github.com/yuichiroTCY/react-redux-pouch-example

以下,解説
大事だと思ったことをバラバラ書いているので一貫性はありません

Presentational componentとContainer componentの分離

別記事にしました:http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60

本プロジェクトでは,
src/componentsにPresentational componentを,src/containersにContainer componentを格納しています.

Reduxの枠組みでPouchDBを扱う

http://redux.js.org/docs/advanced/AsyncActions.html
のとおりです.
Reduxのdispatchはそのままでは非同期APIコールを含むような処理を扱えません.
redux-thunkというMiddlewareを使います.

Middlewareはsrc/store/configureStore.jsで設定しています.

その上で,たとえば,type='note'なドキュメントの一覧を取得する処理はこのように書けます.

src/actions.js
export const POUCH_REQUEST_FETCH_ALL_NOTES = 'POUCH_REQUEST_FETCH_ALL_NOTES'
export const POUCH_RECEIVE_FETCH_ALL_NOTES = 'POUCH_RECEIVE_FETCH_ALL_NOTES'
export function pouchRequestFetchAllNotes() {
    return {
        type: POUCH_REQUEST_FETCH_ALL_NOTES,
    }
}
export function pouchReceiveFetchAllNotes(res) {
    return {
        type: POUCH_RECEIVE_FETCH_ALL_NOTES,
        noteList: res.rows.map(row => row.doc)
    }
}

export function pouchFetchAllNotes() {
    return dispatch => {
        dispatch(pouchRequestFetchAllNotes())

        return db.query({
            map: ((doc) => {
                emit(doc.type)
            }).toString()
        }, {key: 'note', include_docs: true}).then(res => {
            dispatch(pouchReceiveFetchAllNotes(res))
        }).catch(err => {
            console.log(err)
        })
    }
}

このpouchFetchAllNotesアクションを一覧表示用コンポーネントsrc/components/NoteList.react.jscomponentWillMount()dispatch()します.
(正確には,NoteList.react.jscomponentWillMount()からsrc/containers/SyncedNoteList.react.jsonComponentWillMount()が呼ばれます.

src/containers/SyncedNoteList.react.js
const mapDispatchToProps = (dispatch) => {
    return {
        onComponentWillMount: () => {
            dispatch(pouchFetchAllNotes())
        },
// 略
    }
}

react-thunkを使うと,dispatch()に関数を渡せるようになります.
dispatch()に関数を渡すと,その関数がdispatchを引数にして実行されます.
この関数内で非同期処理や参照透過でない処理を書くことができます.

上の例では,まずdispatch(pouchRequestFetchAllNotes())して非同期処理のフラグを立て(Reducerはsrc/store/reducers/note-list.js),その後PouchDBに処理をリクエストしています.この処理はPromiseで非同期に実行されますが,結果が返ってきたタイミングでthen内のdispatch(pouchReceiveFetchAllNotes(res))が実行され,非同期処理フラグをOffにすると同時に結果をstateに格納します(これもReducerはsrc/store/reducers/note-list.js
非同期処理フラグはstore.getState().noteList.isFetchingですが,これを参照することで,非同期処理中に画面にLoading表示をすることができます.

Reduxのreducerの処理は参照透過な同期処理でなくてはなりません.
ある時点のstateとactionが与えられたら,次のstateは予測可能に決定されます.

なので,非同期処理・参照透過でない処理(ランダムなID採番など)はreducer以外の場所に書かなくてはいけません.
これをreduxの枠組みで扱うために,dispatch()に関数を渡してその中で非同期処理・参照透過でない処理を書く仕組みが必要なのです.

ちなみに,今回のコードでは新規データのput時のID採番をsrc/containers/NoteList.react.jsに書いてしまっています.本来はこれもactionに含めるべきなのかもしれません.

React-router

詳細画面のURLに現在閲覧しているノートのIDを持たせ,詳細画面を開いたらその情報を基にPouchDBからデータを取ってくるようにします.
ルーティングにreact-routerを使い,さらにreact-router-reduxを入れることでReduxの枠組みで扱います.
react-router-reduxはルーティングパラメータをreduxのstateとして扱えるようにするMiddlewareを提供します.
Middlewareの設定はsrc/store/configureStore.jsです.

例えば,一覧画面でタイトルをクリックしたら詳細画面に飛ぶ処理は以下のように書けます.

src/containers/SyncedNoteList.react.js
const mapDispatchToProps = (dispatch) => {
    return {
// 略
        onListItemClick: (_id) => {
            dispatch(push('notes/' + _id))
        }
    }
}

pushというAction Creatorが提供されるので,reduxのdispatchとしてページ遷移を書けます.

また,遷移した先の詳細画面はURLがnotes/:noteIdですが,この:noteIdprops.params.noteIdとして渡されるので,componentWillMount()でこのIDを使ってdispatch(pouchGetNote(id))します.
pouchGetNote(id)は,前節で説明したとおり非同期にPouchDBの処理を行うアクションです.
これによって,ページ遷移と同時にPouchDBにリクエストを投げ,結果をstateに格納して,stateを参照して表示する,という処理になります.リクエスト中はstate内のisFetchingフラグを立てて,画面に"Fetching..."と表示します(src/components/NoteForm.react.js)

src/containers/SyncedNoteForm.react.js
const mapStateToProps = (state, ownProps) => {
    return {
        noteId: ownProps.params.noteId,
        isFetching: state.noteForm.isFetching,
        note: state.noteForm.note,
    }
}

const mapDispatchToProps = dispatch => {
    return {
        onComponentWillMount: id => {
            dispatch(pouchGetNote(id))
        },
    }
}
src/components/NoteForm.react.js
    componentWillMount() {
        this.props.onComponentWillMount(this.props.noteId)
    },

また,前節との合わせ技として,PouchDBの処理が完了したらページ遷移する,みたいなこともできます.
本アプリは,詳細画面で内容を保存したら一覧画面に戻るようになっていますが,その部分の処理は以下のようになっています.

src/containers/SyncedNoteForm.react.js

const mapDispatchToProps = dispatch => {
    return {
// 略
        onSaveClick: (note) => {
            dispatch(pouchPutNote(note))
                .then(() => {
                    dispatch(push('/'))
                })
        },
// 略
    }
}

dispatch(pouchPutNote(note))の部分ですが,pouchPutNote(note)が内部でPouchDBの非同期処理を書いてPromiseを返すので,そのPromiseにそのままメソッドチェーンで非同期処理完了後の処理を書くことができます.
これによって,PouchDBへのPutが完了したあと,/へ遷移する,ということができます.

PouchDBのchangesで自動同期

src/app.js

src/app.js
// Subscribe PouchDB change event
db.changes({
    since: 'now',
    live: true,
}).on('change', change => {
    // Very idiot and inefficient way to sync.
    // In product app, this should be more sophisticated like: http://pouchdb.com/2015/02/28/efficiently-managing-ui-state-in-pouchdb.html
    store.dispatch(pouchFetchAllNotes())
}).on('error', err => {
    console.log(err)
});

のようにして,リモートのPouchDBの変更を監視しています.
変更があったら,一覧取得のアクションをdispatchします.
変更があったら一覧を再取得するだけなので,

  • 詳細画面で編集中の内容がリモートで変更された場合に対応できない
  • 一つ変更がある度に一覧をすべて取得しなおすので非効率

など問題がありますが,自動同期処理を書く叩き台としてこんな感じかな,と.

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9