A Simple React/Redux/RxJs Timer Part 2

So, a while back, I made an extremely simple web timer which you can find on https://obscure-temple-46876.herokuapp.com/. The code for it is on https://github.com/NerdcoreSteve/SimpleWebTimer. I blogged about it here.

I've been using it every day. The problem is that I neglected to add a reset button. Well today I'm going to change that and blog about it while I'm doing it!

Since it's a React/Redux project, adding this is going to be pretty simple. First I'm going to add a new action to the reducer:

 67             case 'RESET':
 68                 return reset(state.interval)

reset is just going to create a state like my initialState but it's going to keep the current interval.

 29     reset = interval => ({
 30         time: 0,
 31         paused: true,
 32         interval: interval * 60,
 33         text: `${interval}`,
 34     }),

In fact, I think I'll use reset in creating my initial state just to keep my code DRY.

 28     initialInterval = 25,
 29     reset = interval => ({
 30         time: 0,
 31         paused: true,
 32         interval: interval * 60,
 33         text: `${interval}`,
 34     }),
 35     initialState = reset(initialInterval),
 36     reducer = (state = initialState, action) => {

Now all I have to do is add a button that will do the reset. I think I'll have it available all the time, no matter whether the timer is going or not.

134                 <button  
135                     onClick={() => store.dispatch({  
136                         type: 'RESET'  
137                     })}  
138                     type="button">  
139                         Reset  
140                 </button>  

Wait, I'm running into trouble, see this bit?

 32         interval: interval * 60,

I'm switching between minutes and seconds in the code and it's screwing the interval up when I reset. I just need to convert to minutes when I display and use seconds everywhere else. Let me clean this up a bit...

Okay, now here's how it looks now:

 28     initialInterval = 25 * 60,
 29     reset = interval => ({
 30         time: 0,
 31         paused: true,
 32         interval: interval,
 33         text: `${interval / 60}`,
 34     }),
 35     initialState = reset(initialInterval),
 36     reducer = (state = initialState, action) => {

The code deals with seconds only. The only time we change to minutes is for display purposes.

RESET is handled by a saga now:

 75     timerSaga = function* (action) {
 76         if(action.type === 'START_RESUME'
               && store.getState().paused) {
 77             timer.resume()
 78             yield put({type: 'STARTED_RESUMED'})
 79         } else if(action.type === 'PAUSE'
                      && !store.getState().paused) {
 80             timer.pause()
 81             yield put({type: 'PAUSED'})
 82         } else if(action.type === 'RESET') {
 83             if(!store.getState().paused) {
 84                 timer.pause()
 85             }
 86             yield put({type: 'PAUSED'})
 87             yield put({type: 'RESET_STATE'})
 88         }
 89     },

This is because we interact with the timer via a saga. And I do that because turning a timer on and off is a side-effect, not something that belongs in the reducer.

The saga sends two actions, first it pauses the app, next it resets the state with RESET_STATE.

 63             case 'RESET_STATE':
 64                 return reset(state.interval)

That was our RESET_STATE is what our RESET action was before.

 69             case 'RESET_STATE':
 70                 return reset(state.interval)

I also had to change CHANGE_INTERVAL, the bit that resets the interval to some other number of minutes.

 58             case 'CHANGE_INTERVAL':
 59                 const
 60                     parsedInterval = 
 61                     newInterval = 
                                ? parsedInterval
                                : state.interval
 62                 return reset(newInterval * 60)

But this really looks like it should be handled by an Either from FolkTaleJS

 59             case 'CHANGE_INTERVAL':
 60                 return Right(state)
 61                     .map(R.prop('text'))
 62                     .map(parseInt)
 63                     .chain(interval =>
 64                         isNaN(interval)
 65                             ? Left(state.interval)
 66                             : Right(interval))
 67                     .map(R.multiply(60))
 68                     .fold(reset, reset)

I realize some of y'all might not be sold on the idea that this is simpler or easier to understand, but it is for us functional peeps. :)

I briefly touched on Either in another post but it has two parts. Left and Right. if you map on a Right, it maps just like you would expect an array to. If you map on a Left, the function is not applied to that Left's contents.

fold unboxes an Either, no matter if it's a Left or a right. It acts like map except it uses two functions for it's transformations. The first function is used if it's a Left, the second is used if it's a Right.

So, with that explanation out of the way...

First we put state into a Right. Then we grab it's text prop. next we parseInt it. Next, if the parseInted result is not a number we return a Left of the original state interval. If it is a number we return a Right of the parsed number.

If we had used map instead of chain we'd either have a Right(Right(interval)) or a Right(Left(state.interval)) so we use chain, which is like map, but it unboxes by one level. I talk about chain briefly in this post.

The upshot is that we'll either have a Right(interval) or a Left(state.interval). We then multiply the interval by 60 (again, only the right side) and fold out with reset on both Left and Right.

To me, this is pretty simple and very elegant, but to each their own.

Anyway now there's a reset button for my timer! Happy coding!

Looking for a software developer?