私は、タイマーをたとえば60秒から0にカウントダウンしてから一部のコンテンツを変更したいアプリケーションに取り組んでいます。その後、タイマーは60で再開します。
私はこれをReact=とFluxに実装しましたが、これが初めてなので、まだいくつかの問題が発生しています。
次に、タイマーの開始/停止ボタンを追加します。タイマーの状態をどこに置く/処理するのかわかりません。
コンポーネントTimer.jsx
は次のようになります。
var React = require('react');
var AppStore = require('../stores/app-store.js');
var AppActions = require('../actions/app-actions.js');
function getTimeLeft() {
return {
timeLeft: AppStore.getTimeLeft()
}
}
var Timer = React.createClass({
_tick: function() {
this.setState({ timeLeft: this.state.timeLeft - 1 });
if (this.state.timeLeft < 0) {
AppActions.changePattern();
clearInterval(this.interval);
}
},
_onChange: function() {
this.setState(getTimeLeft());
this.interval = setInterval(this._tick, 1000);
},
getInitialState: function() {
return getTimeLeft();
},
componentWillMount: function() {
AppStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
componentDidMount: function() {
this.interval = setInterval(this._tick, 1000);
},
render: function() {
return (
<small>
({ this.state.timeLeft })
</small>
)
}
});
module.exports = Timer;
それは私が単に持っているストアからカウントダウン期間を取得します:
var _timeLeft = 60;
さて、開始/停止ボタンを実装したい場合、Fluxアクションを使用してこれも実装する必要があるように感じますよね?だから私は私の店でこのようなものを持っていることを考えていました:
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.START_TIMER:
// do something
break;
case AppConstants.STOP_TIMER:
// do something
break;
case AppConstants.CHANGE_PATTERN:
_setPattern();
break;
}
AppStore.emitChange();
return true;
})
ただし、Timerコンポーネントは現在setIntervalを処理しているため、START/STOP_TIMERイベントを機能させる方法がわかりません。 TimerコンポーネントからストアにsetIntervalを移動し、どういうわけかこれをコンポーネントに渡しますか?
完全なコードは here にあります。
私はあなたのコードをダウンロードし、あなたが望むスタート/ストップ/リセット機能を実装することになりました。私はそれがおそらく説明する最良の方法だと思います-いくつかのコメントとともに実行してテストできるコードを示すことです。
私は実際には2つの実装になりました。それらを実装Aと実装Bと呼びます。
両方の実装を示すのは興味深いと思いました。うまくいけば、あまり混乱を招くことはありません。
記録としては、実装Aがより良いバージョンです。
ここでは、両方の実装について簡単に説明します。
実装A
このバージョンは、Appコンポーネントレベルで状態を追跡します。タイマーは、props
をTimerコンポーネントに渡すことによって管理されます。ただし、タイマーコンポーネントは、それ自体の残り時間を追跡します。
実装B
このバージョンでは、コンポーネントの状態とイベントを管理するために、TimerStoreおよびTimerActionモジュールを使用して、Timerコンポーネントレベルでのタイマーの状態を追跡します。
実装Bの大きな(そしておそらく致命的な)欠点は、Timerコンポーネントを1つしか持てないことです。これは、TimerStoreおよびTimerActionモジュールが本質的にシングルトンであるためです。
実装A
このバージョンは、Appコンポーネントレベルで状態を追跡します。ここのコメントのほとんどは、このバージョンのコードにあります。
タイマーは、props
をタイマーに渡すことによって管理されます。
この実装のコード変更リスト:
app-constants.js
ここで、タイマーをリセットするための定数を追加しました。
_module.exports = {
START_TIMER: 'START_TIMER',
STOP_TIMER: 'STOP_TIMER',
RESET_TIMER: 'RESET_TIMER',
CHANGE_PATTERN: 'CHANGE_PATTERN'
};
_
app-actions.js
タイマーのリセットアクションを処理するためのディスパッチメソッドを追加しました。
_var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppActions = {
changePattern: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.CHANGE_PATTERN
})
},
resetTimer: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.RESET_TIMER
})
},
startTimer: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.START_TIMER
})
},
stopTimer: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.STOP_TIMER
})
}
};
module.exports = AppActions;
_
app-store.js
ここで状況が少し変わります。変更を加えた場所に詳細なコメントをインラインで追加しました。
_var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');
// I added a TimerStatus model (probably could go in its own file)
// to manage whether the timer is "start/stop/reset".
//
// The reason for this is that reset state was tricky to handle since the Timer
// component no longer has access to the "AppStore". I'll explain the reasoning for
// that later.
//
// To solve that problem, I added a `reset` method to ensure the state
// didn't continuously loop "reset". This is probably not very "Flux".
//
// Maybe a more "Flux" alternative is to use a separate TimerStore and
// TimerAction?
//
// You definitely don't want to put them in AppStore and AppAction
// to make your timer component more reusable.
//
var TimerStatus = function(status) {
this.status = status;
};
TimerStatus.prototype.isStart = function() {
return this.status === 'start';
};
TimerStatus.prototype.isStop = function() {
return this.status === 'stop';
};
TimerStatus.prototype.isReset = function() {
return this.status === 'reset';
};
TimerStatus.prototype.reset = function() {
if (this.isReset()) {
this.status = 'start';
}
};
var CHANGE_EVENT = "change";
var shapes = ['C', 'A', 'G', 'E', 'D'];
var rootNotes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
var boxShapes = require('../data/boxShapes.json');
// Added a variable to keep track of timer state. Note that this state is
// managed by the *App Component*.
var _timerStatus = new TimerStatus('start');
var _pattern = _setPattern();
function _setPattern() {
var rootNote = _getRootNote();
var shape = _getShape();
var boxShape = _getBoxForShape(shape);
_pattern = {
rootNote: rootNote,
shape: shape,
boxShape: boxShape
};
return _pattern;
}
function _getRootNote() {
return rootNotes[Math.floor(Math.random() * rootNotes.length)];
}
function _getShape() {
return shapes[Math.floor(Math.random() * shapes.length)];
}
function _getBoxForShape(shape) {
return boxShapes[shape];
}
// Simple function that creates a new instance of TimerStatus set to "reset"
function _resetTimer() {
_timerStatus = new TimerStatus('reset');
}
// Simple function that creates a new instance of TimerStatus set to "stop"
function _stopTimer() {
_timerStatus = new TimerStatus('stop');
}
// Simple function that creates a new instance of TimerStatus set to "start"
function _startTimer() {
_timerStatus = new TimerStatus('start');
}
var AppStore = merge(EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
// Added this function to get timer status from App Store
getTimerStatus: function() {
return _timerStatus;
},
getPattern: function() {
return _pattern;
},
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.RESET_TIMER:
// Handle reset action
_resetTimer();
break;
case AppConstants.START_TIMER:
// Handle start action
_startTimer();
break;
case AppConstants.STOP_TIMER:
// Handle stop action
_stopTimer();
break;
case AppConstants.CHANGE_PATTERN:
_setPattern();
break;
}
AppStore.emitChange();
return true;
})
});
module.exports = AppStore;
_
App.jsx
App.jsxには多くの変更があり、特にタイマーコンポーネントからアプリコンポーネントに状態を移動しました。再びコード内の詳細なコメント。
_var React = require('react');
var Headline = require('./components/Headline.jsx');
var Scale = require('./components/Scale.jsx');
var RootNote = require('./components/RootNote.jsx');
var Shape = require('./components/Shape.jsx');
var Timer = require('./components/Timer.jsx');
// Removed AppActions and AppStore from Timer component and moved
// to App component. This is done to to make the Timer component more
// reusable.
var AppActions = require('./actions/app-actions.js');
var AppStore = require('./stores/app-store.js');
// Use the AppStore to get the timerStatus state
function getAppState() {
return {
timerStatus: AppStore.getTimerStatus()
}
}
var App = React.createClass({
getInitialState: function() {
return getAppState();
},
// Listen for change events in AppStore
componentDidMount: function() {
AppStore.addChangeListener(this.handleChange);
},
// Stop listening for change events in AppStore
componentWillUnmount: function() {
AppStore.removeChangeListener(this.handleChange);
},
// Timer component has status, defaultTimeout attributes.
// Timer component has an onTimeout event (used for changing pattern)
// Add three basic buttons for Start/Stop/Reset
render: function() {
return (
<div>
<header>
<Headline />
<Scale />
</header>
<section>
<RootNote />
<Shape />
<Timer status={this.state.timerStatus} defaultTimeout="15" onTimeout={this.handleTimeout} />
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<button onClick={this.handleClickReset}>Reset</button>
</section>
</div>
);
},
// Handle change event from AppStore
handleChange: function() {
this.setState(getAppState());
},
// Handle timeout event from Timer component
// This is the signal to change the pattern.
handleTimeout: function() {
AppActions.changePattern();
},
// Dispatch respective start/stop/reset actions
handleClickStart: function() {
AppActions.startTimer();
},
handleClickStop: function() {
AppActions.stopTimer();
},
handleClickReset: function() {
AppActions.resetTimer();
}
});
module.exports = App;
_
Timer.jsx
Timer
とAppStore
の依存関係を削除してAppActions
コンポーネントを再利用しやすくしたため、Timer
にも多くの変更が加えられました。詳細なコメントはコードにあります。
_var React = require('react');
// Add a default timeout if defaultTimeout attribute is not specified.
var DEFAULT_TIMEOUT = 60;
var Timer = React.createClass({
// Normally, shouldn't use props to set state, however it is OK when we
// are not trying to synchronize state/props. Here we just want to provide an option to specify
// a default timeout.
//
// See http://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html)
getInitialState: function() {
this.defaultTimeout = this.props.defaultTimeout || DEFAULT_TIMEOUT;
return {
timeLeft: this.defaultTimeout
};
},
// Changed this to `clearTimeout` instead of `clearInterval` since I used `setTimeout`
// in my implementation
componentWillUnmount: function() {
clearTimeout(this.interval);
},
// If component updates (should occur when setState triggered on Timer component
// and when App component is updated/re-rendered)
//
// When the App component updates we handle two cases:
// - Timer start status when Timer is stopped
// - Timer reset status. In this case, we execute the reset method of the TimerStatus
// object to set the internal status to "start". This is to avoid an infinite loop
// on the reset case in componentDidUpdate. Kind of a hack...
componentDidUpdate: function() {
if (this.props.status.isStart() && this.interval === undefined) {
this._tick();
} else if (this.props.status.isReset()) {
this.props.status.reset();
this.setState({timeLeft: this.defaultTimeout});
}
},
// On mount start ticking
componentDidMount: function() {
this._tick();
},
// Tick event uses setTimeout. I find it easier to manage than setInterval.
// We just keep calling setTimeout over and over unless the timer status is
// "stop".
//
// Note that the Timer states is handled here without a store. You could probably
// say this against the rules of "Flux". But for this component, it just seems unnecessary
// to create separate TimerStore and TimerAction modules.
_tick: function() {
var self = this;
this.interval = setTimeout(function() {
if (self.props.status.isStop()) {
self.interval = undefined;
return;
}
self.setState({timeLeft: self.state.timeLeft - 1});
if (self.state.timeLeft <= 0) {
self.setState({timeLeft: self.defaultTimeout});
self.handleTimeout();
}
self._tick();
}, 1000);
},
// If timeout event handler passed to Timer component,
// then trigger callback.
handleTimeout: function() {
if (this.props.onTimeout) {
this.props.onTimeout();
}
}
render: function() {
return (
<small className="timer">
({ this.state.timeLeft })
</small>
)
},
});
module.exports = Timer;
_
実装B
コード変更リスト:
app-constants.js
これらは、Timerコンポーネントを処理するため、おそらくtimer-constants.jsという名前のファイルに入れる必要があります。
_module.exports = {
START_TIMER: 'START_TIMER',
STOP_TIMER: 'STOP_TIMER',
RESET_TIMER: 'RESET_TIMER',
TIMEOUT: 'TIMEOUT',
TICK: 'TICK'
};
_
timer-actions.js
このモジュールは自明です。タイムアウト、ティック、リセットの3つのイベントを追加しました。詳細については、コードを参照してください。
_var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');
module.exports = {
// This event signals when the timer expires.
// We can use this to change the pattern.
timeout: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.TIMEOUT
})
},
// This event decrements the time left
tick: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.TICK
})
},
// This event sets the timer state to "start"
start: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.START_TIMER
})
},
// This event sets the timer state to "stop"
stop: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.STOP_TIMER
})
},
// This event resets the time left and sets the state to "start"
reset: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.RESET_TIMER
})
},
};
_
timer-store.js
タイマーをAppStore
から分離しました。これは、Timerコンポーネントをもう少し再利用可能にするためです。
タイマーストアは、次の状態を追跡します。
タイマーストアは、次のイベントを処理します。
これがコードです:
_var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');
var CHANGE_EVENT = "change";
var TIMEOUT_SECONDS = 15;
var _timerStatus = 'start';
var _timeLeft = TIMEOUT_SECONDS;
function _resetTimer() {
_timerStatus = 'start';
_timeLeft = TIMEOUT_SECONDS;
}
function _stopTimer() {
_timerStatus = 'stop';
}
function _startTimer() {
_timerStatus = 'start';
}
function _decrementTimer() {
_timeLeft -= 1;
}
var TimerStore = merge(EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
getTimeLeft: function() {
return _timeLeft;
},
getStatus: function() {
return _timerStatus;
},
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.START_TIMER:
_startTimer();
break;
case AppConstants.STOP_TIMER:
_stopTimer();
break;
case AppConstants.RESET_TIMER:
_resetTimer();
break;
case AppConstants.TIMEOUT:
_resetTimer();
break;
case AppConstants.TICK:
_decrementTimer();
break;
}
TimerStore.emitChange();
return true;
})
});
module.exports = TimerStore;
_
app-store.js
これは_pattern-store.js
_という名前にすることができますが、再利用可能にするためにいくつかの変更を行う必要があります。具体的には、タイマーのTIMEOUT
アクション/イベントを直接リッスンして、パターンの変更をトリガーしています。パターンの変更を再利用する場合は、おそらくその依存関係を望まないでしょう。たとえば、ボタンなどをクリックしてパターンを変更したい場合などです。
それ以外は、タイマー関連のすべての機能をAppStore
から削除しました。
_var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');
var CHANGE_EVENT = "change";
var shapes = ['C', 'A', 'G', 'E', 'D'];
var rootNotes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
var boxShapes = require('../data/boxShapes.json');
var _pattern = _setPattern();
function _setPattern() {
var rootNote = _getRootNote();
var shape = _getShape();
var boxShape = _getBoxForShape(shape);
_pattern = {
rootNote: rootNote,
shape: shape,
boxShape: boxShape
};
return _pattern;
}
function _getRootNote() {
return rootNotes[Math.floor(Math.random() * rootNotes.length)];
}
function _getShape() {
return shapes[Math.floor(Math.random() * shapes.length)];
}
function _getBoxForShape(shape) {
return boxShapes[shape];
}
var AppStore = merge(EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
getPattern: function() {
return _pattern;
},
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.TIMEOUT:
_setPattern();
break;
}
AppStore.emitChange();
return true;
})
});
module.exports = AppStore;
_
App.jsx
ここで、開始/停止/リセット用のボタンをいくつか追加しました。クリックすると、TimerActionがディスパッチされます。したがって、「停止」ボタンをクリックすると、TimerAction.stop()
が呼び出されます
_var React = require('react');
var Headline = require('./components/Headline.jsx');
var Scale = require('./components/Scale.jsx');
var RootNote = require('./components/RootNote.jsx');
var Shape = require('./components/Shape.jsx');
var Timer = require('./components/Timer.jsx');
var TimerActions = require('./actions/timer-actions.js');
var App = React.createClass({
render: function() {
return (
<div>
<header>
<Headline />
<Scale />
</header>
<section>
<RootNote />
<Shape />
<Timer />
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<button onClick={this.handleClickReset}>Reset</button>
</section>
</div>
);
},
handleClickStart: function() {
TimerActions.start();
},
handleClickStop: function() {
TimerActions.stop();
},
handleClickReset: function() {
TimerActions.reset();
}
});
module.exports = App;
_
Timer.jsx
主な変更点の1つは、元々使用されていたAppActionとAppStoreの代わりに、TimerActionとTimerStoreを使用していることです。その理由は、Timerコンポーネントをもう少し再利用可能にしようとするためです。
タイマーには次の状態があります。
setTimeout
の代わりにsetInterval
を使用したことに注意してください。 setTimeout
の方が管理しやすいと思います。
ロジックの大部分は__tick
_メソッドにあります。基本的に、ステータスが「開始」である限り、setTimeout
を呼び出し続けます。
タイマーがゼロに達すると、timeout
イベントを通知します。 TimerStoreとAppStoreはこのイベントをリッスンしています。
タイマーがゼロに達していない場合は、「ティック」イベントを通知して1秒を減算します。
最後に、タイマーが停止してから開始する場合を処理する必要があります。これは、componentDidUpdate
フックを介して処理できます。このフックは、コンポーネントの状態が変化したとき、または親コンポーネントが再レンダリングされたときに呼び出されます。
componentDidUpdate
メソッドでは、ステータスが「start」でタイムアウト識別子が未定義の場合にのみ、「ticking」を開始するようにしています。複数のsetTimeoutsを実行したくありません。
_var React = require('react');
var TimerActions = require('../actions/timer-actions.js');
var TimerStore = require('../stores/timer-store.js');
function getTimerState() {
return {
status: TimerStore.getStatus(),
timeLeft: TimerStore.getTimeLeft()
}
}
var Timer = React.createClass({
_tick: function() {
var self = this;
this.interval = setTimeout(function() {
if (self.state.status === 'stop') {
self.interval = undefined;
return;
}
if (self.state.timeLeft <= 0) {
TimerActions.timeout();
} else {
TimerActions.tick();
}
self._tick();
}, 1000);
},
getInitialState: function() {
return getTimerState();
},
componentDidMount: function() {
TimerStore.addChangeListener(this.handleChange);
this._tick();
},
componentWillUnmount: function() {
clearTimeout(this.interval);
TimerStore.removeChangeListener(this.handleChange);
},
handleChange: function() {
this.setState(getTimerState());
},
componentDidUpdate: function() {
if (this.state.status === 'start' && this.interval === undefined) {
this._tick();
}
},
render: function() {
return (
<small className="timer">
({ this.state.timeLeft })
</small>
)
}
});
module.exports = Timer;
_
フラックスを使用する主な理由の1つはアプリケーションの状態を集中化するです。そのためには、コンポーネントのsetState
関数を使用しないようにしますにする必要があります。さらに、コンポーネントが独自の状態を保存する範囲で、それは非常に一時的な性質の状態データに対してのみであるべきです(たとえば、マウスがホバーしているかどうかを示す状態をコンポーネントにローカルに設定する場合があります)。
Fluxでは、ストアは同期的であることを意味します。 (これはFlux実装間でやや論争の的になる点ですが、ストアを同期にすることを強くお勧めします。ストアで非同期操作を許可すると、単方向のデータフローが中断され、アプリケーションの推論が損なわれます。)代わりに、非同期操作はAction Creatorで実行する必要があります。あなたのコードでは、アクションクリエーターについての言及がないので、これが混乱の原因であるのではないかと思います。それにもかかわらず、実際のTimerはアクションクリエーターに存在する必要があります。コンポーネントがタイマーに影響を与える必要がある場合、コンポーネントはアクション作成者のメソッドを呼び出すことができ、アクション作成者はタイマーを作成/管理でき、タイマーはstoreによって処理されるイベントをディスパッチできます。
Update:2014年のreact-conf Fluxパネルで、大規模なFluxアプリケーションに取り組んでいる1人の開発者が、その特定のアプリケーションでは非同期データフェッチを許可すると言っていることに注意してくださいストアでの操作(GETですが、PUTまたはPOSTではありません)。
タイマーをストアから削除し、今のところ、そこでパターンを管理します。タイマーコンポーネントには、いくつかの小さな変更が必要です。
var Timer = React.createClass({
_tick: function() {
if (this.state.timeLeft < 0) {
AppActions.changePattern();
clearInterval(this.interval);
} else {
this.setState({ timeLeft: this.state.timeLeft - 1 });
}
},
_onChange: function() {
// do what you want with the pattern here
// or listen to the AppStore in another component
// if you need this somewhere else
var pattern = AppStore.getPattern();
},
getInitialState: function() {
return { timeLeft: 60 };
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
componentDidMount: function() {
this.interval = setInterval(this._tick, 1000);
AppStore.addChangeListener(this._onChange);
},
render: function() {
return (
<small>
({ this.state.timeLeft })
</small>
)
}
});