私は反応と還元でストップウォッチを作ろうとしています。私はそのようなものをどうやってデザインするかを理解するのに苦労しています。
最初に頭に浮かんだのは、START_TIMER
アクションは、初期のoffset
値を設定します。その直後、setInterval
を使用してTICK
アクションを何度も起動し、オフセットを使用して経過時間を計算し、それを現在の時刻に追加して、 offset
。
このアプローチは機能しているようですが、それを停止する間隔をどのようにクリアするかはわかりません。また、このデザインは貧弱で、おそらくもっと良い方法があるようです。
ここに完全な JSFiddle があり、START_TIMER
機能しています。私のレデューサーが今どのように見えるかを確認したい場合は、次のとおりです。
const initialState = {
isOn: false,
time: 0
};
const timer = (state = initialState, action) => {
switch (action.type) {
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset
};
case 'STOP_TIMER':
return {
...state,
isOn: false
};
case 'TICK':
return {
...state,
time: state.time + (action.time - state.offset),
offset: action.time
};
default:
return state;
}
}
私は本当に助けていただければ幸いです。
私はおそらくこれを別の方法で行うことをお勧めします:経過時間を計算するために必要な状態のみをストアに保存し、コンポーネントにown間隔を設定させます多くの場合、表示を更新したいと考えています。
これにより、アクションのディスパッチが最小限に抑えられ、タイマーを開始および停止(およびリセット)するアクションのみがディスパッチされます。新しい状態オブジェクトを返す毎回アクションをディスパッチし、各connect
edコンポーネントが再レンダリングされることを忘れないでください(ただし、ラップされたコンポーネント内での再レンダリングが多すぎることを避けるために最適化を使用しています)。さらに、他のアクションと一緒にすべてのTICK
sを処理する必要があるため、多くの多くのアクションディスパッチはアプリの状態変化のデバッグを困難にする可能性があります。
次に例を示します。
// Action Creators
function startTimer(baseTime = 0) {
return {
type: "START_TIMER",
baseTime: baseTime,
now: new Date().getTime()
};
}
function stopTimer() {
return {
type: "STOP_TIMER",
now: new Date().getTime()
};
}
function resetTimer() {
return {
type: "RESET_TIMER",
now: new Date().getTime()
}
}
// Reducer / Store
const initialState = {
startedAt: undefined,
stoppedAt: undefined,
baseTime: undefined
};
function reducer(state = initialState, action) {
switch (action.type) {
case "RESET_TIMER":
return {
...state,
baseTime: 0,
startedAt: state.startedAt ? action.now : undefined,
stoppedAt: state.stoppedAt ? action.now : undefined
};
case "START_TIMER":
return {
...state,
baseTime: action.baseTime,
startedAt: action.now,
stoppedAt: undefined
};
case "STOP_TIMER":
return {
...state,
stoppedAt: action.now
}
default:
return state;
}
}
const store = createStore(reducer);
アクションの作成者とレデューサーはプリミティブ値のみを扱い、いかなる種類の間隔やTICK
アクションタイプも使用しないことに注意してください。これで、コンポーネントはこのデータを簡単にサブスクライブして、必要な頻度で更新できます。
// Helper function that takes store state
// and returns the current elapsed time
function getElapsedTime(baseTime, startedAt, stoppedAt = new Date().getTime()) {
if (!startedAt) {
return 0;
} else {
return stoppedAt - startedAt + baseTime;
}
}
class Timer extends React.Component {
componentDidMount() {
this.interval = setInterval(this.forceUpdate.bind(this), this.props.updateInterval || 33);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const { baseTime, startedAt, stoppedAt } = this.props;
const elapsed = getElapsedTime(baseTime, startedAt, stoppedAt);
return (
<div>
<div>Time: {elapsed}</div>
<div>
<button onClick={() => this.props.startTimer(elapsed)}>Start</button>
<button onClick={() => this.props.stopTimer()}>Stop</button>
<button onClick={() => this.props.resetTimer()}>Reset</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
const { baseTime, startedAt, stoppedAt } = state;
return { baseTime, startedAt, stoppedAt };
}
Timer = ReactRedux.connect(mapStateToProps, { startTimer, stopTimer, resetTimer })(Timer);
同じデータに異なる更新頻度で複数のタイマーを表示することもできます。
class Application extends React.Component {
render() {
return (
<div>
<Timer updateInterval={33} />
<Timer updateInterval={1000} />
</div>
);
}
}
この実装で working JSBin をここで確認できます: https://jsbin.com/dupeji/12/edit?js,output
大きなアプリでこれを使用する場合、パフォーマンスの問題のためにrequestAnimationFrame
ではなくsetInterval
を使用します。ミリ秒を表示しているので、モバイルデバイスではデスクトップブラウザーではそれほど気になりません。
更新されたJSFiddle
clearInterval
(一意の識別子)への呼び出しの結果を受け取り、その間隔がそれ以上実行されないようにする setInterval
関数を使用します。
そのため、start()
内でsetInterval
を宣言する代わりに、それをレデューサーに渡して、IDを状態に保存できるようにします。
_interval
をアクションオブジェクトのメンバーとしてディスパッチャーに渡します
start() {
const interval = setInterval(() => {
store.dispatch({
type: 'TICK',
time: Date.now()
});
});
store.dispatch({
type: 'START_TIMER',
offset: Date.now(),
interval
});
}
START_TIMER
アクションレデューサー内の新しい状態にinterval
を保存します
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset,
interval: action.interval
};
______
interval
に従ってコンポーネントをレンダリングする
コンポーネントのプロパティとしてinterval
を渡します。
const render = () => {
ReactDOM.render(
<Timer
time={store.getState().time}
isOn={store.getState().isOn}
interval={store.getState().interval}
/>,
document.getElementById('app')
);
}
次に、outコンポーネント内の状態を調べて、プロパティinterval
があるかどうかに応じてレンダリングします。
render() {
return (
<div>
<h1>Time: {this.format(this.props.time)}</h1>
<button onClick={this.props.interval ? this.stop : this.start}>
{ this.props.interval ? 'Stop' : 'Start' }
</button>
</div>
);
}
______
タイマーの停止
タイマーを停止するには、clearInterval
を使用して間隔をクリアし、initialState
を再度適用するだけです。
case 'STOP_TIMER':
clearInterval(state.interval);
return {
...initialState
};
______
更新されたJSFiddle
Andykenwardの回答と同様に、私はrequestAnimationFrame
を使用してパフォーマンスを向上させます。ほとんどのデバイスのフレームレートは1秒あたり約60フレームしかないからです。しかし、私はできるだけReduxに入れません。イベントをディスパッチする間隔だけが必要な場合は、Reduxではなくコンポーネントレベルですべて実行できます。 この答え のDan Abramovのコメントを参照してください。
以下は、カウントダウンクロックを表示し、期限が切れたときに何かを行うカウントダウンタイマーコンポーネントの例です。 start
、tick
、またはstop
内で、Reduxで起動する必要があるイベントをディスパッチできます。タイマーを開始するときにのみ、このコンポーネントをマウントします。
class Timer extends Component {
constructor(props) {
super(props)
// here, getTimeRemaining is a helper function that returns an
// object with { total, seconds, minutes, hours, days }
this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
}
// Wait until the component has mounted to start the animation frame
componentDidMount() {
this.start()
}
// Clean up by cancelling any animation frame previously scheduled
componentWillUnmount() {
this.stop()
}
start = () => {
this.frameId = requestAnimationFrame(this.tick)
}
tick = () => {
const timeLeft = getTimeRemaining(this.props.expiresAt)
if (timeLeft.total <= 0) {
this.stop()
// dispatch any other actions to do on expiration
} else {
// dispatch anything that might need to be done on every tick
this.setState(
{ timeLeft },
() => this.frameId = requestAnimationFrame(this.tick)
)
}
}
stop = () => {
cancelAnimationFrame(this.frameId)
}
render() {...}
}