redux-ship icon indicating copy to clipboard operation
redux-ship copied to clipboard

Is there best practice to use setInterval() in redux-ship?

Open kuy opened this issue 9 years ago • 2 comments

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;
  }
}

kuy avatar Nov 08 '16 05:11 kuy

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.js We 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.js We add the dispatch function itself as a parameter of Effect.run:

    function dispatch(action) {
      store.dispatch(action);
      Ship.run(
        action => Effect.run(action, dispatch),
        store.dispatch,
        store.getState,
        logControl(control)(action)
      );
    }
    
  • controllers.js We start a new periodic timer on TIME_PLAY and clear the current timer on TIME_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.js We add the following initial state:

    app: {
      interval: null,
      tick: 0,
      time: false,
    },
    

    and the following app reducers:

    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.

clarus avatar Nov 08 '16 13:11 clarus

@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).

eliseumds avatar Feb 28 '17 12:02 eliseumds