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'
なドキュメントの一覧を取得する処理はこのように書けます.
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.js
のcomponentWillMount()
でdispatch()
します.
(正確には,NoteList.react.js
のcomponentWillMount()
からsrc/containers/SyncedNoteList.react.js
のonComponentWillMount()
が呼ばれます.
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
です.
例えば,一覧画面でタイトルをクリックしたら詳細画面に飛ぶ処理は以下のように書けます.
const mapDispatchToProps = (dispatch) => {
return {
// 略
onListItemClick: (_id) => {
dispatch(push('notes/' + _id))
}
}
}
push
というAction Creatorが提供されるので,reduxのdispatchとしてページ遷移を書けます.
また,遷移した先の詳細画面はURLがnotes/:noteId
ですが,この:noteId
がprops.params.noteId
として渡されるので,componentWillMount()
でこのIDを使ってdispatch(pouchGetNote(id))
します.
pouchGetNote(id)
は,前節で説明したとおり非同期にPouchDBの処理を行うアクションです.
これによって,ページ遷移と同時にPouchDBにリクエストを投げ,結果をstateに格納して,stateを参照して表示する,という処理になります.リクエスト中はstate内のisFetchingフラグを立てて,画面に"Fetching..."と表示します(src/components/NoteForm.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))
},
}
}
componentWillMount() {
this.props.onComponentWillMount(this.props.noteId)
},
また,前節との合わせ技として,PouchDBの処理が完了したらページ遷移する,みたいなこともできます.
本アプリは,詳細画面で内容を保存したら一覧画面に戻るようになっていますが,その部分の処理は以下のようになっています.
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
で
// 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します.
変更があったら一覧を再取得するだけなので,
- 詳細画面で編集中の内容がリモートで変更された場合に対応できない
- 一つ変更がある度に一覧をすべて取得しなおすので非効率
など問題がありますが,自動同期処理を書く叩き台としてこんな感じかな,と.