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.
You want to use the
clearInterval
function which takes the result of a call tosetInterval
(a unique identifier) and stops that interval from executing any further.So rather than declare a
setInterval
withinstart()
, instead pass it to the reducer so that it can store its ID on the state:Pass
interval
to dispatcher as a member of the action objectStore
interval
on new state within theSTART_TIMER
action reducer______
Rendering the component according to
interval
Pass in
interval
as a property of the component:We can then inspect the state within out component to render it according to whether there is a property
interval
or not:______
Stopping the timer
To stop the timer we clear the interval using
clearInterval
and simply apply theinitialState
again:______
Updated JSFiddle
https://jsfiddle.net/8z16xwd2/2/
Similar to andykenward's answer, I would use
requestAnimationFrame
to improve performance as the frame rate of most devices is only about 60 frames per second. However, I would put as little in Redux as possible. If you just need the interval to dispatch events, you can do that all at the component level instead of in Redux. See Dan Abramov's comment in this answer.Below is an example of a countdown Timer component that both shows a countdown clock and does something when it has expired. Inside the
start
,tick
, orstop
you can dispatch the events that you need to fire in Redux. I only mount this component when the timer should start.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
connect
ed 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 theTICK
s alongside the other actions.Here's an example:
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:You could even display multiple timers on the same data with a different update frequency:
You can see a working JSBin with this implementation here: https://jsbin.com/dupeji/12/edit?js,output
If you are going to use this in a bigger app then I would use
requestAnimationFrame
instead of ansetInterval
for performance issues. As you are showing milliseconds you would notice this on mobile devices not so much on desktop browsers.Updated JSFiddle
https://jsfiddle.net/andykenward/9y1jjsuz