Ad

Creating A Stopwatch With Redux

- 1 answer

I've been trying to make a stopwatch in react and redux. I've been having trouble trouble figuring out how to design such a thing in redux.

The first thing that came to mind was having a START_TIMER action which would set the initial offset value. Right after that, I use setInterval to fire off a TICK action over and over again that calculates how much time has passed by using the offset, adds it to the current time, and then updates the offset.

This approach seems to work, but I'm not sure how I would clear the interval to stop it. Also, it seems like this design is poor and there is probably a better way to do it.

Here is a full JSFiddle that has the START_TIMER functionality working. If you just want to see what my reducer looks like right now, here it is:

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

I would really appreciate any help.

Ad

Answer

I would probably recommend going about this differently: store only the state necessary to calculate the elapsed time in the store, and let components set their own interval for however often they wish to update the display.

This keeps action dispatches to a minimum — only actions to start and stop (and reset) the timer are dispatched. Remember, you're returning a new state object every time you dispatch an action, and each connected component then re-renders (even though they use optimizations to avoid too many re-renders inside the wrapped components). Furthermore, many many action dispatches can make it difficult to debug app state changes, since you have to deal with all the TICKs alongside the other actions.

Here's an example:

// 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);

Notice the action creators and reducer deals only with primitive values, and does not use any sort of interval or a TICK action type. Now a component can easily subscribe to this data and update as often as it wants:

// 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);

You could even display multiple timers on the same data with a different update frequency:

class Application extends React.Component {
  render() {
    return (
      <div>
        <Timer updateInterval={33} />
        <Timer updateInterval={1000} />
      </div>
    );
  }
}

You can see a working JSBin with this implementation here: https://jsbin.com/dupeji/12/edit?js,output

Ad
source: stackoverflow.com
Ad