Is there best practice to use setInterval() in redux-ship?
Hi, I'm new to redux-ship and have an experience of redux-saga.
I want to create a periodic timer which dispatches TIME_TICK action every second with redux-ship.
In redux-ship, however, a controller and an effect handler are event-driven.
Is there a good solution to handle setInterval() in redux-ship way?
Here is my example using setTimeout() instead of setInterval().
Project: https://github.com/kuy/lifegame-redux
Source Code: https://github.com/kuy/lifegame-redux/tree/master/src/ship
// From controllers.js
export default function* control(action) {
switch (action.type) {
case TIME_PLAY: {
while (yield* Ship.getState(selectors.time)) {
yield* Ship.call({ type: Effect.DELAY });
yield* Ship.commit(timeTick());
}
return;
}
default:
return;
}
}
// From effects.js
function delay(msec) {
return new Promise(resolve => {
setTimeout(() => {
resolve(msec);
}, msec);
});
}
export async function run(effect) {
switch (effect.type) {
case DELAY:
return await delay(PERIOD);
default:
return;
}
}
Hi,
I think using a while and setTimeout is a reasonable implementation of a periodic timer. To use setInterval directly, here is roughly how I would do:
-
effects.jsWe define two effects to set and remove an interval. The interval handler is the call of a given Redux Ship action:export async function run(effect, dispatch) { switch (effect.type) { case 'SET_INTERVAL': return setInterval(() => dispatch(effect.action), effect.period); case 'CLEAR_INTERVAL': clearInterval(effect.id); break; default: return; } } -
index.jsWe add thedispatchfunction itself as a parameter ofEffect.run:function dispatch(action) { store.dispatch(action); Ship.run( action => Effect.run(action, dispatch), store.dispatch, store.getState, logControl(control)(action) ); } -
controllers.jsWe start a new periodic timer onTIME_PLAYand clear the current timer onTIME_PAUSE:import * as Ship from 'redux-ship'; import { TIME_PLAY, TIME_PAUSE, timeTick } from 'common/actions'; import { PERIOD } from 'common/constants'; export default function* control(action) { switch (action.type) { case TIME_PLAY: { const currentInterval = yield* Ship.getState(state => state.app.interval); if (!currentInterval) { const interval = yield* Ship.call({ type: 'SET_INTERVAL', action: timeTick(), period: PERIOD, }); yield* Ship.commit({ type: 'INTERVAL_ADD', id: interval, }); } return; } case TIME_PAUSE: { const currentInterval = yield* Ship.getState(state => state.app.interval); if (currentInterval) { yield* Ship.call({ type: 'CLEAR_INTERVAL', id: currentInterval, }); yield* Ship.commit({ type: 'INTERVAL_REMOVE', }); } return; } default: return; } } -
reducers.jsWe add the following initial state:app: { interval: null, tick: 0, time: false, },and the following
appreducers:INTERVAL_ADD: (state, action) => { return { ...state, interval: action.id }; }, INTERVAL_REMOVE: (state, action) => { return { ...state, interval: null }; },
Notice that there are no generic ways to "cancel" a task in Redux Ship. Thus we need to use clearInterval explicitly. This is a design choice with the aim to make cancellation "more explicit, less magic" and avoid stopping pending ships.
@kuy, there's another issue that @clarus' implementation solves: clock sync. You'd run into problem if you wanted to track the total running time after TIME_PLAY gets dispatched, for example, because the application loses some time by simply yielding the effects delay and commit(timeTick()) (I'd say some 2-3ms for each iteration, but it adds up after a while).