Skip to content

Creating a stopwatch without side effects in the reducer? #1194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
zaynv opened this issue Jan 3, 2016 · 16 comments
Closed

Creating a stopwatch without side effects in the reducer? #1194

zaynv opened this issue Jan 3, 2016 · 16 comments
Labels

Comments

@zaynv
Copy link

zaynv commented Jan 3, 2016

I've been trying to create a stopwatch with React/Redux. I have a working example in a JSFiddle here. The approach I took was to have a START_TIMER action which in turn uses setInterval to fire off TICK actions which calculate how much time has passed and add that to the current time.

I'm not sure if this approach is valid because I do a clearInterval in the STOP_TIMER part of my reducer, which I assume is a side effect, and my reducer is no longer a pure function. I am pretty new to redux, so I may just be thinking about doing this the wrong way. Is my current example a bad practice in redux, and is there a better way to achieve what I am trying to do? Thanks a lot.

@winstonewert
Copy link

Calling clearInterval inside the reducer is certainly a bad practice, as it makes the reducer non-pure.

A common way and straightforward way here would be to clear the interval inside your stop() function before you call dispatch.

I've forked your fiddle and implemented what I'd do: https://jsfiddle.net/uu71ka1e/1/. Basically, I add another subscriber to the react store, and have it cancel or start the time depending on whether the isOn property is true in your state.

@gaearon
Copy link
Contributor

gaearon commented Jan 4, 2016

The answer by @winstonewert is correct.
For posterity I'll copy the code sample here:

const { createStore } = Redux;

// Initial state for reducer
const initialState = {
  isOn: false,
  time: 0
};

// Reducer function
const timer = (state = initialState, action) => {
  switch (action.type) {
    case 'START_TIMER':
      return {
        ...initialState,
        isOn: true,
        offset: action.offset,
      };

    case 'STOP_TIMER':
      return {
        isOn: false,
        time: state.time
      };

    case 'TICK':
      return {
        ...state,
        time: state.time + (action.time - state.offset),
        offset: action.time
      };

    default: 
      return state;
  }
}

// Create store using the reducer
const store = createStore(timer);

// React Component to display the timer
class Timer extends React.Component {
  constructor() {
    super();
    this.start = this.start.bind(this);
    this.stop = this.stop.bind(this);
  }

  start() {
    store.dispatch({
      type: 'START_TIMER',
      offset: Date.now(),
    });
  }

  stop() {
    store.dispatch({
      type: 'STOP_TIMER'
    });
  }

  format(time) {
    const pad = (time, length) => {
      while (time.length < length) {
        time = '0' + time;
      }
      return time;
    }

    time = new Date(time);
    let m = pad(time.getMinutes().toString(), 2);
    let s = pad(time.getSeconds().toString(), 2);
    let ms = pad(time.getMilliseconds().toString(), 3);

    return `${m} : ${s} . ${ms}`;
  }

  render() {
    return (
      <div>
        <h1>Time: {this.format(this.props.time)}</h1>
        <button onClick={this.props.isOn ? this.stop : this.start}>
          { this.props.isOn ? 'Stop' : 'Start' }
        </button>
      </div>
    );
  }
}

// render function that runs everytime an action is dispatched
const render = () => {
  ReactDOM.render(
    <Timer 
      time={store.getState().time}
      isOn={store.getState().isOn}
      interval={store.getState().interval}
    />,
    document.getElementById('app')
  );
}

store.subscribe(render);

var interval = null;
store.subscribe(() => {
    if (store.getState().isOn && interval === null) {
      interval = setInterval(() => {
        store.dispatch({
          type: 'TICK',
          time: Date.now()
        });
      });
    }
    if (!store.getState().isOn && interval !== null) {
      clearInterval(interval);
      interval = null;
    }
});

render();

@QuotableWater7
Copy link

@winstonewert your example was super helpful to me today, thank you!

@aph-vsn
Copy link

aph-vsn commented Nov 29, 2016

Hey guy I really appreciate this example! I am getting a syntax error at ...initialState and at ...state

case 'START_TIMER':
return {
...initialState,
isOn: true,
offset: action.offset,
};

case 'TICK':
return {
...state,
time: state.time + (action.time - state.offset),
offset: action.time
};

Thanks for your help!

@winstonewert
Copy link

@aph-vsn, that is new syntax that may not be supported by your development environment. Are you using babel or another transpiler?

@markerikson
Copy link
Contributor

@aph-vsn : Yeah, the Object Spread operator is a not-yet-final piece of syntax. See http://redux.js.org/docs/recipes/UsingObjectSpreadOperator.html for more information on using it. Also, note that the Create-React-App tool includes the plugin needed to use the Object Spread operator, so that works out of the box in projects started using Create-React-App.

@aph-vsn
Copy link

aph-vsn commented Nov 29, 2016

@winstonewert
I am using babel.

@TrySound
Copy link
Contributor

Just use Object.assign({}, state, { timer: 123 }) instead. For me it's more readable.

@aph-vsn
Copy link

aph-vsn commented Nov 29, 2016

Ok I fixed the ...initialState.

I got a different problem now. I am using the same code here.

Uncaught TypeError: Cannot read property 'subscribe' of undefined(…)

@markerikson
Copy link
Contributor

@aph-vsn : A couple suggestions:

First, you should use triple backticks to properly format code blocks on Github, like:

```js
// code here
```

Second, the discussion is probably best moved to Stack Overflow at this point - you'll get better help there trying to work out code issues.

@aph-vsn
Copy link

aph-vsn commented Nov 29, 2016

ok, if you have an idea, please let me know! Thank you

@TrySound
Copy link
Contributor

TrySound commented Nov 29, 2016

Third, what do you use for bundling? Is there no one error in your terminal? You have named export store but trying to import default.

@aph-vsn
Copy link

aph-vsn commented Nov 29, 2016

No error in my terminal.

@aph-vsn
Copy link

aph-vsn commented Nov 29, 2016

if anybody would like to help me : here is the stackoverflow https://stackoverflow.com/questions/40873488/reactjs-uncaught-typeerror-cannot-read-property-subscribe-of-undefined

@aph-vsn
Copy link

aph-vsn commented Nov 29, 2016

I got it. Thank you @TrySound you were right

@cauldyclark15
Copy link

clicking "Stop" then "Start" again making it start again from 00:00.000, I think the right script for 'START_TIMER' is
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset,
};
NOT
case 'START_TIMER':
return {
...initialState,
isOn: true,
offset: action.offset,
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants