ノートアプリを作りながら学んだ3つのこと
最近、Electron + ReactJS構成でノートアプリを作ってます。
拙作で使うのは両方とも初めてですが、軽く概要だけ知った状態から始めました。
そのためいろいろ転んだわけですが、たぶんこれから始め(てい)るみなさんにも参考になるんじゃないかと思ったので、その失敗をシェアしたいと思います。
もっといい方法あるよ!とかコメント欄などで教えていただけると幸いです 😉
本エントリでは、以下の3つのケースで自分がやりがちな失敗パターンを紹介します。
- 非同期処理の中途ステートを取り扱う
- アクションの結果を複数のストアに反映させる
- ReactJS/fluxと親和性の高いAPIを設計する
ただし、Flux実装は alt.js の使用を前提とします。
では、それぞれ詳しく説明します!
1. 非同期処理の中途ステートを取り扱う
データを非同期に取ってきて表示するケースは多いと思います。
上画像の例では、リストをクリックした直後に項目のハイライトだけ先にやってその後にデータを読み込んでエディタ領域に表示しています。こうすることで、UIの動作を軽快に感じさせることが出来ます。
この例で失敗パターンを説明します。
Reactコンポーネント側で途中ステートを保持「するな」
最初、読み込み途中のステートをReactコンポーネント側で取り扱おうとしてたんですが、後で間違いだと気づきました。
以下のようなコードをイメージしてください(詳細は省きます):
class NoteListView extends React.Component {
componentDidMount () {
this.unlisten = editorStore.listen(::this.handleEditorStoreChange);
}
componentWillUnmount () {
this.unlisten()
}
render () {
const viewItems = this.props.notes.map((note, index) => {
return (
<NoteListItemView
{ ...note }
key={ index }
active={ note.id === this.state.selectedNoteId }
onClick={ this.handleClick.bind(this, note) }>
</NoteListItemView>
)
})
return ({ viewItems })
}
handleClick (note) {
editorActions.show(note.id);
this.setState({ selectedNoteId: note.id });
}
handleEditorStoreChange () {
this.setState({ selectedNoteId: editorStore.getState().note.id });
}
}
class EditorActions {
open (noteId) {
return async (dispatch)-> {
/* (読み込み処理) */
dispatch(loadedNoteData);
}
}
}
class EditorStore {
open (loadedNoteData) {
this.setState({ note: loadedNoteData });
}
}
一見よさそうですね。でもダメです。
例えばカーソルキーを素早く連打して前のノート読み込みが終わる前に次のノートを開こうとすると、問題が起きます。
この実装では、ノートが開き終わるとhandleEditorStoreChange
でリストビューの選択状態が反映されます。
なので、A->B
とノートを素早く選択したとき、一瞬選択状態がAに戻る現象が起こります。
きもちわるいですね!
fluxでの非同期処理の書き方の例
今日のflux実装の乱立から察せられるように、決まった答えは無いのであくまで一例です。
alt.jsでも推奨される実装方法が一応あるのですが、大げさなので自分はいつも以下のように組んでいます:
class EditorActions {
open (noteId) {
return async (dispatch)-> {
dispatch(noteId);
try {
/* (読み込み処理) */
this.openSuccess(loadedNoteData)
} catch (err) {
this.openFailure(err)
}
}
}
}
class EditorStore {
open (noteId) {
this.setState({ openingNoteId: noteId });
}
openSuccess (loadedNoteData) {
if (this.openingNoteId === loadedNoteData.id) {
this.setState({ noteId: loadedNoteData.id, note: loadedNoteData, openingNoteId: null });
}
}
openFailure (err) {
this.setState({ lastError: err });
}
}
// 変更箇所のみ記載
class NoteListView extends React.Component {
componentWillMount () {
this.updateSelection()
}
handleClick (note) {
editorActions.show(note.id);
}
updateSelection () {
const editorState = editorStore.getState()
this.setState({ selectedNoteId: editorState.openingNoteId || editorState.noteId });
}
}
これで、EditorStoreが読み込みステートを保持しているので、選択状態が後戻りしたりすることは無くなりました!
2. アクションの結果を複数のストアに反映させる
複数のUIコンポーネントが連動するような処理を書きたいケースって多いですよね。
特に親子関係ではなくて兄弟関係のコンポーネントのケースについてです。
コンポーネントごとにActionやStoreを用意している場合、それぞれの連動をどうするか悩みがちだと思います。
上画像の例では、ノートへのリンクをクリックしたらエディタでリンク先ノートを読み込みつつ、ノートリストの選択状態も反映させるという動作です。
この例から失敗パターンを学んでいきます。
ビューを通じてストアの変更駆動でアクションを「呼び出すな」
fluxにおいて、コンポーネントを連動させるためにやりがちな悪い処理のフローは以下のようなイメージです。
例えばノートのリンクをクリックして:
- (1) エディタを開くアクションを呼び出す:
editorActions.open()
- (2) ノートリストがノートを開いた事を検知:
handleEditorStoreChange
- (3) 選択状態を更新するアクションを更に呼び出す:
noteListActions.updateSelection()
というような流れです。
ただ、連続してアクションを呼び出すとaltに「dispatch中にアクションは呼び出せねぇよ」と怒られてしまいますので、deferをかませています。
データの更新のためだけにViewが関わるべきではない
ではこの設計の何が悪いのかというと、上記フローチャートで(2)のビューは(3)のアクションを呼び出すことしかしていません。もはやビューでやる必要性がない上に、これはビューの役割ではありません。
ただし、選択状態の保持をノートリスト用ストアではなくコンポーネント内ステートで行っている場合は正しいでしょう。
Dispatcherを使ってアクション結果を複数ストアに反映させる
ここでお察しの良い方はお気づきかと思いますが、そうです、上記チャートではDispatcherの存在が無視されています!!
altが良くも悪くもよく出来ているおかげで、Dispatcherの概念を意識しなくても使えてしまえる節があります。
このDispatcherを正しく使うことがこの失敗を乗り越えるキーです。
Dispatcherは、StoreにAction結果を橋渡しする役割があります。概念上、橋渡しするストアは複数設定できます。alt.jsでもbindListeners
メソッドを使うことでこれを実現しています。
この機構によって、以下のようなフローを組むことが出来ます:
つまりノートのリンクをクリックして:
- (1) エディタを開くアクションを呼び出す:
editorActions.open()
- (2) アクション結果をエディタ用ストアとノートリスト用ストアにディスパッチ
- それぞれコンポーネントに反映
というような流れです。シンプルですね!
これたぶん初心者がやりがちな失敗だと思うんですよねー。
3. ReactJS/fluxと親和性の高いAPIを設計する
この話はまだ気づいたばかりで、具体例とかあんまり出せないんですが、メモ代わりに書いておきます。
ウェブでもデスクトップでも、アプリがサーバとやりとりすることはよくあると思います。
その際に、みなさんおそらくサーバ側にHTTP APIを用意しますよね。
REST APIを「間違って実装するな」
「RESTってあれですよね、HTTPでJSON返すやつっしょ?」って思っている人、絶対多いです。
例えばこんなAPIを実装したりしてません?
/posts/create
/posts/get
/users/get-userid-from-maddr
ダメですよ!
RESTが何の略か言えない人、TwitterのAPIを参考にしてる人(俺です)も、いますぐ復習しましょう!
CouchDBのAPIとか涙でよく読めないぐらい素晴らしいです:
RESTに準拠することはFluxと相性がいい
REST(REpresentational State Transferの頭字語)からも示されているように、これはRPCによるステートの転送なんですよね。
だから、アプリのステートを取り扱うfluxとリモートサーバのステートを転送するRESTは、取り扱うデータの性質が似ている。
それはつまり、APIとFluxの設計はある程度対応付けられるという事だと思うんですよね。
それによって設計に一貫性が生まれ、無駄なコードが減ってバグも抑えることが出来るんじゃないでしょうか。
accountActions
からは、ユーザのアカウントに関することだから/account/*
しか呼び出さない。sessionActions
からは、認証に関することだから/session/*
しか呼び出さない。
こういうシンプルで分かりやすいルールを設ければ、設計にも迷いが減るのではないでしょうか。
あと、1つのアクションから複数のAPIを呼び出すような設計もおかしい(無駄がある)可能性が高いです。
1アクションあたり1 APIと決めておくのも、実装をシンプルに保つ効果があるかもしれません。
参考になったサイト
おまけです。
少し使い慣れてきたかなという時に見るとすごく納得感を持って読めたドキュメントです。
設計に迷ったら読むとベター。
まとめ:もっと失敗しよう
自分が経験した中で、みなさんにより共通しそうな失敗談を挙げてみました。参考になれば幸いです。
ReactJS/fluxは正しく理解すれば、ほんとうにバグを減らしてくれるいい設計思想だなあと実感しつつあります。
jQueryやBackboneとかAngularも好きだけどね。
またこれからどんな失敗が待っているか分かりませんが、その折にはまたこのような形でシェアしたいと思います。
最後まで読んでくださりありがとうございました!