はじめに
素のReactだと面倒。でもFluxやReduxを使う程でもない規模のプロジェクトで何を使うか、というテーマに対する最適解を紹介します(胡散臭げな顔で)。
小規模なReact拡張ライブラリとして、例えばreact-micro-containerやflumptなどがあります。
これらはフレームワークというよりはReactアプリケーションの実装を手助けする補助的役割がメインで、素のReactを徒歩とすると、react-micro-containerやflumptは自転車的な位置付けになるかと思います。
それに対し、FluxやReduxなどの本格的なフレームワークは2tトラックとかそんなところでしょうか。
徒歩は論外、自転車でもちょっとしんどい、かといって2tトラックは大袈裟なんだよなぁ・・・といった時の選択肢として今回紹介するのが拙作のNanoxです。
規模としてはバイクとか軽自動車とか、そのくらいを想像していただければ(分かりやすいようで分かりにくい謎の例え)。
図にするとこんな感じです。
使用方法
Nanoxはreact-micro-containerをベースにしたフレームワークなので、こちらの記事をベースにどんなものか説明していきます。
基本部分は大体同じです。
子コンポーネント
react-micro-containerの記事から抜粋
まず普通のステートレスなReactコンポーネントを作る。
dispatch
というprops
を受け取ってそれを通してイベントを発火するというのが唯一の規約。components/counter.jsimport React from 'react'; // Stateless component export default class Counter extends React.Component { render() { return ( <div> <div>{this.props.count}</div> <button onClick={() => this.props.dispatch('increment', 1)}>+1</button> <button onClick={() => this.props.dispatch('decrement', 1)}>-1</button> <button onClick={() => this.props.dispatch('increment', 100)}>+100</button> </div> ); } }
ここはNanoxでも全く同じです。
コンテナコンポーネント
react-micro-containerの記事から抜粋
それをwrapするコンテナコンポーネントを作る。こいつはstateを持ち、子のコンポーネントに
dispatch
を渡してイベントをsubscribeし、適時stateを更新する。container/counter.jsimport MicroContainer from 'react-micro-container'; import Counter from '../components/counter'; export default class CounterContainer extends MicroContainer { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { this.subscribe({ increment: this.handleIncrement, decrement: this.handleDecrement, }); } handleIncrement(count) { this.setState({ count: this.state.count + count }); } handleDecrement(count) { this.setState({ count: this.state.count - count }); } render() { return <Counter dispatch={this.dispatch} {...this.state} />; } }
ここからNanox独自の実装が入ってきますので、拡張ポイントを説明していきます。
上記のreact-micro-containerでの実装部分をNanoxでは以下のように書きます。
import Nanox from 'nanox'; // 変更点①
import Counter from '../components/counter';
import actions from './actions'; // 変更点②
export default class CounterContainer extends Nanox { // 変更点③
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
this.registerActions(actions); // 変更点④
}
// 変更点⑤
render() {
return <Counter dispatch={this.dispatch} {...this.state} />;
}
}
変更点①
react-micro-container
をインポートするかわりにnanox
をインポートします。
変更点②
actions.js
という外部ファイルをインポートしています。react-micro-container版ではコンテナコンポーネント内に直接記述していたhandleIncrement
とhandleDecrement
をこちらのactions.js
に切り出しています。
変更点③
変更点①に合わせて、MicroContainer
を継承していた部分をNanox
を継承するように変更しています。
※Nanox
はMicroContainer
を継承していくつかの機能拡張を付加したクラスです。
変更点④
react-micro-container版ではthis.subscribe()
でイベント登録を行っているところを、Nanox版ではthis.registerActions()
というメソッドでイベント登録を行っています。
見た感じはthis.subscribe()
と同じように見えますが、this.registerActions()
では内部で色々といい感じにやってくれた後にthis.subscribe()
でイベント登録をしています。
具体的に何がいい感じなのかについては後述します。
変更点⑤
変更点②に合わせて、react-micro-container版で記述していたhandleIncrement
とhandleDecrement
がなくなっています。
マウント
react-micro-containerの記事から抜粋
このコンポーネントも単なるReact Componentなのでこのコンテナを普通にマウントするだけ。
app.jsimport ReactDOM from 'react-dom'; import CounterContainer from './container/counter'; ReactDOM.render(<CounterContainer />, document.getElementById('app'));
こちらもreact-micro-container版とNanox版で違いはありません。
まとめ
以上のように、Nanoxでreact-micro-containerから拡張したのはコンテナコンポーネントの部分だけという事になります。
それ以外はreact-micro-containerと同じ使用感で書くことができます。
Nanoxにおけるアクション登録方法
上記使用方法で紹介したコンテナコンポーネントの説明でさらっと流していた部分について掘り下げていきます。
actions.jsの内容
上記変更点②でactions.js
という外部ファイルにイベント処理を切り出しました。ここではそのactions.js
の内容について説明します。
以下はreact-micro-container版でのhandleIncrement
とhandleDecrement
を同期、非同期で実装したイベントのサンプルです。
なお、以降はアクションという名称で呼んでいきます。
const actions = {
increment(count) { // ポイント①
const currentState = this.getState(); // ポイント②
return { // ポイント③
count: currentState.count + count
};
},
decrement(count) {
return new Promise((resolve, reject) => { // ポイント④
setTimeout(() => {
const currentState = this.getState();
resolve({ // ポイント⑤
count: currentState.count - count
});
}, 1000);
});
}
};
// ポイント⑥
export default actions;
ポイント①
increment
というのがアクション名です。子コンポーネントから実行されるthis.dispatch()
で指定されるやつです。
コレ
↓
<button onClick={() => this.props.dispatch('increment', 1)}>+1</button>
アクション名の後に指定されている引数のcount
はthis.dispatch()
でアクション名の後に指定されたものです。複数指定も可能です。
複数指定した場合
↓
<button onClick={() => this.props.dispatch('increment', 1, 2, 3)}>+1</button>
複数指定した場合のアクションでの受け取り方は以下のようになります。
increment(x, y, z) {
// x = 1, y = 2, z = 3
ポイント②
アクション内ではthis.getState()
というメソッドによって現在のコンテナコンポーネントの最新のstate
を取得できます。
なお、ここで取得したstate
はコンテナコンポーネントのstate
のコピーなので、この値を直接変更してもコンテナコンポーネントのstate
は更新されません。
ポイント③
このアクション内で更新したい部分だけをオブジェクトで返すことにより、コンテナコンポーネントのstate
が更新されます。
ポイント④
Promise
を返すことにより非同期アクションの実装も可能です。
ポイント⑤
Promise
内の処理で、ポイント③と同様に更新したい部分のオブジェクトをresolve
することによりコンテナコンポーネントのstate
を更新することができます。
ポイント⑥
アクションをエクスポートしてコンテナコンポーネントからインポートできるようにします。
まとめ
ぶっちゃけ、別ファイルに切り出さずにコンテナコンポーネント内のthis.registerActions()
に直接アクションをぶっこんでしまってもよいのですが、コンポーネント部分は極力描画だけに専念してもらおうという計らいです。
this.registerActions()
のいい感じなところ
1. シンプルなアクションハンドラ
react-micro-containerのthis.subscribe()
で登録するイベントはあくまでもただのイベントなので、各イベントハンドラがthis.setState()
でコンテナコンポーネントのstate
を更新する所まで面倒を見る必要があります。
Nanoxのthis.registerActions()
で登録したアクションは更新する部分のみを返すだけで良いというのと、非同期処理もPromise
で包むだけで良いので、毎回同じようなお作法的処理を書く必要がなく、処理がスッキリします。
2. アクション名のTypoを検知
react-micro-containerのthis.subscribe()
で登録するイベントはあくまでもただのイベントなので(2回目)、this.dispatch()
に指定するイベント名を間違えたとしても何も起きないだけで、イベント名を間違えたかどうかというところまでは分かりません。
Nanoxではthis.registerActions()
で登録したアクション以外のアクション名がthis.dispatch()
で指定された時にエラーで教えてくれます。
3. 自動エラーハンドリング
Nanoxのthis.registerActions()
時にこっそりとエラーハンドリング用の隠しアクション(__error
)が登録されます。
ユーザーが登録したアクション内で例外が発生したり、Promise
がreject
された時には自動的にこの__error
アクションがdispatch
されます。
各アクション内で個々にtry
~catch
しなくても、まとめて__error
がエラーの面倒を見てくれます。逆に面倒を見てほしくない場合は自前でtry
〜catch
して例外を捕捉すればOKです。
4. エラーハンドラのカスタマイズ
Nanoxデフォルトの__error
アクションはエラー内容をconsole.error
で表示するだけですが、ユーザー側で登録するアクションに__error
という名称のアクションを実装することによってこの挙動を上書きすることができます。
まだ質問なんてないけどFAQ
Q. アクション内の処理開始時と終了時にsetState()
したいんだけど
A. 例えばAjaxでサーバーからデータを取得する処理の開始時に「通信中」的な表示をするためにコンテナコンポーネントのstate
に通信中フラグをたてて、通信が終了した時にそのフラグを解除するみたいなやつのことです。
確かに、更新部分を返すことによってコンテナコンポーネントのstate
を更新する形だとこういった2段階にわたっての更新はできないように思えます。
が、アクション内ではthis.dispatch()
によって別のアクションを呼び出すことができるので、上記のようなケースはこのような感じで実装することができます。
const actions = {
.
.
.
getData(url) {
// getDataアクション内でwaitingアクションを呼び出し
this.dispatch('waiting', true);
return new Promise((resolve, reject) => {
fetch(url)
.then((response) => response.json())
.then((json) => {
resolve({
data: json,
waiting: false // 通信終了時にコンテナコンポーネントのwaitingを解除
});
});
});
},
waiting(flag) {
// waitingアクションでコンテナコンポーネントのstateを更新
return {
waiting: flag
};
}
.
.
.
なお、アクション内のthis
は、このdispatch()
と先ほど紹介したgetState()
のみを実装しています。各アクションのあちこちでsetState()
やsubscribe()
を呼び出したり、state
を直接更新するような世紀末カオス状態に陥るのを防止するための制限となります。
デモ
Nanoxのソースリポジトリに同梱されているサンプルのデモになります。
さいごに
大勢のスタッフが関わる大規模アプリケーションならFlux、Reduxのようなガッチリとしたフレームワークがベストチョイスになると思いますが、個人や数人規模で管理していくものであればこれくらいこじんまりしてた方が見通しが良いのではないでしょうか。
まあ、同じようなコンセプト(バイクや軽自動車規模)のFlux系フレームワークはいくらでもあるとは思いますが、その中の選択肢の1つとして、こじんまりとはしつつも無茶はさせない制約と多少の気の利いたエラー検知機能を備えたNanoxを検討してみてはいかがでしょうか、なんて。
おっと忘れてたインストールは、
$ npm install nanox
で、ソースはここにありますのでPRなどありましたらよろしくお願いします。