Handling a timer in React/Flux
Asked Answered
T

3

17

I'm working on an application where I want a timer to countdown from, say, 60 seconds to 0 and then change some content, after which the timer restarts again at 60.

I have implemented this in React and Flux but since I'm new to this, I'm still running into some problems.

I now want to add a start/stop button for the timer. I'm not sure where to put/handle the timer state.

I have a component Timer.jsx which looks like this:

var React = require('react');
var AppStore = require('../stores/app-store.js');
var AppActions = require('../actions/app-actions.js');

function getTimeLeft() {
  return {
    timeLeft: AppStore.getTimeLeft()
  }
}

var Timer = React.createClass({
  _tick: function() {
    this.setState({ timeLeft: this.state.timeLeft - 1 });
    if (this.state.timeLeft < 0) {
      AppActions.changePattern();
      clearInterval(this.interval);
    }
  },
  _onChange: function() {
    this.setState(getTimeLeft());
    this.interval = setInterval(this._tick, 1000);
  },
  getInitialState: function() {
    return getTimeLeft();
  },
  componentWillMount: function() {
    AppStore.addChangeListener(this._onChange);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  componentDidMount: function() {
    this.interval = setInterval(this._tick, 1000);
  },
  render: function() {
    return (
      <small>
        ({ this.state.timeLeft })
      </small>
    )
  }
});

module.exports = Timer;

It retrieves a countdown duration from the store, where I simply have:

var _timeLeft = 60;

Now, when I want to implement a start/stop button, I feel like I should also implement this through Flux Actions, correct? So I was thinking of having something like this in my store:

dispatcherIndex: AppDispatcher.register(function(payload) {
  var action = payload.action;

  switch(action.actionType) {
    case AppConstants.START_TIMER:
      // do something
      break;
    case AppConstants.STOP_TIMER:
      // do something
      break;
    case AppConstants.CHANGE_PATTERN:
      _setPattern();
      break;
  }

  AppStore.emitChange();

  return true;
})

However, since my Timer component currently handles the setInterval, I don't know how to get my START/STOP_TIMER events working. Should I move the setInterval stuff from the Timer component to the Store and somehow pass this down to my component?

Full code can be found here.

Testes answered 22/12, 2014 at 14:15 Comment(5)
Do you need to be able to restore the remaining time on the timer? Say if the store were persisted on the server, should refreshing the page keep track of remaining time? If so, timeLeft probably belongs in the store as well.Terry
I'm not persisting anything on the server. The only thing I want it to be able to start/pause/stop the timer. On refresh, it should just start from 60 seconds again. This is my store as I currently have it: pastebin.com/MwV6cRbeTestes
If no other component needs access to timeLeft I would keep all of that inside your Timer component. Then you could just start and stop the interval. Otherwise, you need to control the interval in the store and dispatch change events.Remediable
@Remediable When my timer starts, I want to send a CHANGE_PATTERN event though, so could I just handle start and stop in my timer component (as well as move timeLeft there from the store) and then do AppStore.changePattern() whenever my timer starts? Or does that go against the whole uni-directional flow of Flux? A tad confused on how to properly solve this. Thanks!Testes
Not sure if this is the Flux way to do it, but maybe provide start/stop/pause/reset state that is managed by root app and have it pass that to timer as a prop. Then you can pass a click event to the button component from the root app. When the button is pressed, update the start/stop/pause state of the app which then triggers a render update where the new start/stop/pause state is passed to timer as a prop. Just musing mostly.Riddle
R
21

I ended up downloading your code and implementing the start/stop/reset feature you wanted. I think that's probably the best way to explain things - to show code that you can run and test along with some comments.

I actually ended up with two implementations. I'll call them Implementation A and Implementation B.

I thought it would be interesting to show both implementations. Hopefully it doesn't cause too much confusion.

For the record, Implementation A is the better version.

Here are brief descriptions of both implementations:

Implementation A

This version keeps track of the state at the App component level. The timer is managed by passing props to the Timer component. The timer component does keep track of it's own time left state though.

Implementation B

This version keeps track of the timer state at the the Timer component level using a TimerStore and TimerAction module to manage state and events of the component.

The big (and probably fatal) drawback of implementation B is that you can only have one Timer component. This is due to the TimerStore and TimerAction modules essentially being Singletons.


|

|

Implementation A

|

|

This version keeps track of the state at the App component level. Most of the comments here are in the code for this version.

The timer is managed by passing props to the Timer.

Code changes listing for this implementation:

  • app-constants.js
  • app-actions.js
  • app-store.js
  • App.jsx
  • Timer.jsx

app-constants.js

Here I just added a constant for reseting the timer.

module.exports = {
  START_TIMER: 'START_TIMER',
  STOP_TIMER: 'STOP_TIMER',
  RESET_TIMER: 'RESET_TIMER',
  CHANGE_PATTERN: 'CHANGE_PATTERN'
};

app-actions.js

I just added a dispatch method for handling the reset timer action.

var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');

var AppActions = {
  changePattern: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.CHANGE_PATTERN
    })
  },
  resetTimer: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.RESET_TIMER
    })
  },
  startTimer: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.START_TIMER
    })
  },
  stopTimer: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.STOP_TIMER
    })
  }
};

module.exports = AppActions;

app-store.js

Here is where things change a bit. I added detailed comments inline where I made changes.

var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');


// I added a TimerStatus model (probably could go in its own file)
// to manage whether the timer is "start/stop/reset".
//
// The reason for this is that reset state was tricky to handle since the Timer
// component no longer has access to the "AppStore". I'll explain the reasoning for
// that later.
//
// To solve that problem, I added a `reset` method to ensure the state
// didn't continuously loop "reset". This is probably not very "Flux".
//
// Maybe a more "Flux" alternative is to use a separate TimerStore and
// TimerAction? 
//
// You definitely don't want to put them in AppStore and AppAction
// to make your timer component more reusable.
//
var TimerStatus = function(status) {
  this.status = status;
};

TimerStatus.prototype.isStart = function() {
  return this.status === 'start';
};

TimerStatus.prototype.isStop = function() {
  return this.status === 'stop';
};

TimerStatus.prototype.isReset = function() {
  return this.status === 'reset';
};

TimerStatus.prototype.reset = function() {
  if (this.isReset()) {
    this.status = 'start';
  }
};


var CHANGE_EVENT = "change";

var shapes = ['C', 'A', 'G', 'E', 'D'];
var rootNotes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];

var boxShapes = require('../data/boxShapes.json');


// Added a variable to keep track of timer state. Note that this state is
// managed by the *App Component*.
var _timerStatus = new TimerStatus('start');


var _pattern = _setPattern();

function _setPattern() {
  var rootNote = _getRootNote();
  var shape = _getShape();
  var boxShape = _getBoxForShape(shape);

  _pattern = {
    rootNote: rootNote,
    shape: shape,
    boxShape: boxShape
  };

  return _pattern;
}

function _getRootNote() {
  return rootNotes[Math.floor(Math.random() * rootNotes.length)];
}

function _getShape() {
  return shapes[Math.floor(Math.random() * shapes.length)];
}

function _getBoxForShape(shape) {
  return boxShapes[shape];
}


// Simple function that creates a new instance of TimerStatus set to "reset"
function _resetTimer() {
  _timerStatus = new TimerStatus('reset');
}

// Simple function that creates a new instance of TimerStatus set to "stop"
function _stopTimer() {
  _timerStatus = new TimerStatus('stop');
}

// Simple function that creates a new instance of TimerStatus set to "start"
function _startTimer() {
  _timerStatus = new TimerStatus('start');
}


var AppStore = merge(EventEmitter.prototype, {
  emitChange: function() {
    this.emit(CHANGE_EVENT);
  },

  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  },


  // Added this function to get timer status from App Store
  getTimerStatus: function() {
    return _timerStatus;
  },


  getPattern: function() {
    return _pattern;
  },

  dispatcherIndex: AppDispatcher.register(function(payload) {
    var action = payload.action;

    switch(action.actionType) {
      case AppConstants.RESET_TIMER:
        // Handle reset action
        _resetTimer();
        break;
      case AppConstants.START_TIMER:
        // Handle start action
        _startTimer();
        break;
      case AppConstants.STOP_TIMER:
        // Handle stop action
        _stopTimer();
        break;
      case AppConstants.CHANGE_PATTERN:
        _setPattern();
        break;
    }

    AppStore.emitChange();

    return true;
  })
});

module.exports = AppStore;

App.jsx

There are numerous changes in App.jsx, specifically we have moved the state to the App component from the timer component. Again detailed comments in the code.

var React = require('react');

var Headline = require('./components/Headline.jsx');
var Scale = require('./components/Scale.jsx');
var RootNote = require('./components/RootNote.jsx');
var Shape = require('./components/Shape.jsx');
var Timer = require('./components/Timer.jsx');


// Removed AppActions and AppStore from Timer component and moved
// to App component. This is done to to make the Timer component more
// reusable.
var AppActions = require('./actions/app-actions.js');
var AppStore = require('./stores/app-store.js');


// Use the AppStore to get the timerStatus state
function getAppState() {
  return {
    timerStatus: AppStore.getTimerStatus()
  }
}

var App = React.createClass({
  getInitialState: function() {
    return getAppState();
  },


  // Listen for change events in AppStore
  componentDidMount: function() {
    AppStore.addChangeListener(this.handleChange);
  },


  // Stop listening for change events in AppStore
  componentWillUnmount: function() {
    AppStore.removeChangeListener(this.handleChange);
  },


  // Timer component has status, defaultTimeout attributes.
  // Timer component has an onTimeout event (used for changing pattern)
  // Add three basic buttons for Start/Stop/Reset
  render: function() {
    return (
      <div>
        <header>
          <Headline />
          <Scale />
        </header>
        <section>
          <RootNote />
          <Shape />
          <Timer status={this.state.timerStatus} defaultTimeout="15" onTimeout={this.handleTimeout} />
          <button onClick={this.handleClickStart}>Start</button>
          <button onClick={this.handleClickStop}>Stop</button>
          <button onClick={this.handleClickReset}>Reset</button>
        </section>
      </div>
    );
  },


  // Handle change event from AppStore
  handleChange: function() {
    this.setState(getAppState());
  },


  // Handle timeout event from Timer component
  // This is the signal to change the pattern.
  handleTimeout: function() {
    AppActions.changePattern();
  },


  // Dispatch respective start/stop/reset actions
  handleClickStart: function() {
    AppActions.startTimer();
  },
  handleClickStop: function() {
    AppActions.stopTimer();
  },
  handleClickReset: function() {
    AppActions.resetTimer();
  }
});

module.exports = App;

Timer.jsx

The Timer has many changes as well since I removed the AppStore and AppActions dependencies to make the Timer component more reusable. Detailed comments are in the code.

var React = require('react');


// Add a default timeout if defaultTimeout attribute is not specified.
var DEFAULT_TIMEOUT = 60;

var Timer = React.createClass({

  // Normally, shouldn't use props to set state, however it is OK when we
  // are not trying to synchronize state/props. Here we just want to provide an option to specify
  // a default timeout.
  //
  // See http://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html)
  getInitialState: function() {
    this.defaultTimeout = this.props.defaultTimeout || DEFAULT_TIMEOUT;
    return {
      timeLeft: this.defaultTimeout
    };
  },


  // Changed this to `clearTimeout` instead of `clearInterval` since I used `setTimeout`
  // in my implementation
  componentWillUnmount: function() {
    clearTimeout(this.interval);
  },

  // If component updates (should occur when setState triggered on Timer component
  // and when App component is updated/re-rendered)
  //
  // When the App component updates we handle two cases:
  // - Timer start status when Timer is stopped
  // - Timer reset status. In this case, we execute the reset method of the TimerStatus
  //   object to set the internal status to "start". This is to avoid an infinite loop
  //   on the reset case in componentDidUpdate. Kind of a hack...
  componentDidUpdate: function() {
    if (this.props.status.isStart() && this.interval === undefined) {
      this._tick();
    } else if (this.props.status.isReset()) {
      this.props.status.reset();
      this.setState({timeLeft: this.defaultTimeout});
    }
  },

  // On mount start ticking
  componentDidMount: function() {
    this._tick();
  },


  // Tick event uses setTimeout. I find it easier to manage than setInterval.
  // We just keep calling setTimeout over and over unless the timer status is
  // "stop".
  //
  // Note that the Timer states is handled here without a store. You could probably
  // say this against the rules of "Flux". But for this component, it just seems unnecessary
  // to create separate TimerStore and TimerAction modules.
  _tick: function() {
    var self = this;
    this.interval = setTimeout(function() {
      if (self.props.status.isStop()) {
        self.interval = undefined;
        return;
      }
      self.setState({timeLeft: self.state.timeLeft - 1});
      if (self.state.timeLeft <= 0) {
        self.setState({timeLeft: self.defaultTimeout});
        self.handleTimeout();
      }
      self._tick();
    }, 1000);
  },

  // If timeout event handler passed to Timer component,
  // then trigger callback.
  handleTimeout: function() {
    if (this.props.onTimeout) {
      this.props.onTimeout();
    }
  }
  render: function() {
    return (
      <small className="timer">
        ({ this.state.timeLeft })
      </small>
    )
  },
});

module.exports = Timer;

|

|

Implementation B

|

|

Code changes listing:

  • app-constants.js
  • timer-actions.js (new)
  • timer-store.js (new)
  • app-store.js
  • App.jsx
  • Timer.jsx

app-constants.js

These should probably go in a file named timer-constants.js since they deal with the Timer component.

module.exports = {
  START_TIMER: 'START_TIMER',
  STOP_TIMER: 'STOP_TIMER',
  RESET_TIMER: 'RESET_TIMER',
  TIMEOUT: 'TIMEOUT',
  TICK: 'TICK'
};

timer-actions.js

This module is self-explanatory. I added three events - timeout, tick, and reset. See code for details.

var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');

module.exports = {

  // This event signals when the timer expires.
  // We can use this to change the pattern.
  timeout: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.TIMEOUT
    })
  },

  // This event decrements the time left
  tick: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.TICK
    })
  },

  // This event sets the timer state to "start"
  start: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.START_TIMER
    })
  },

  // This event sets the timer state to "stop"
  stop: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.STOP_TIMER
    })
  },

  // This event resets the time left and sets the state to "start"
  reset: function() {
    AppDispatcher.handleViewAction({
      actionType: AppConstants.RESET_TIMER
    })
  },
};

timer-store.js

I separated out the timer stuff from the AppStore. This is to make the Timer component a bit more reusable.

The Timer store keeps track of the following state:

  • timer status - Can be "start" or "stop"
  • time left - Time left on timer

The Timer store handles the following events:

  • The timer start event sets timer status to start.
  • The timer stop event sets timer status to stop.
  • The tick event decrements the time left by 1
  • The timer reset event sets the time left to the default and sets timer status to start

Here is the code:

var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');

var CHANGE_EVENT = "change";
var TIMEOUT_SECONDS = 15;

var _timerStatus = 'start';
var _timeLeft = TIMEOUT_SECONDS;

function _resetTimer() {
  _timerStatus = 'start';
  _timeLeft = TIMEOUT_SECONDS;
}

function _stopTimer() {
  _timerStatus = 'stop';
}

function _startTimer() {
  _timerStatus = 'start';
}

function _decrementTimer() {
  _timeLeft -= 1;
}

var TimerStore = merge(EventEmitter.prototype, {
  emitChange: function() {
    this.emit(CHANGE_EVENT);
  },

  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  },

  getTimeLeft: function() {
    return _timeLeft;
  },

  getStatus: function() {
    return _timerStatus;
  },

  dispatcherIndex: AppDispatcher.register(function(payload) {
    var action = payload.action;

    switch(action.actionType) {
      case AppConstants.START_TIMER:
        _startTimer();
        break;
      case AppConstants.STOP_TIMER:
        _stopTimer();
        break;
      case AppConstants.RESET_TIMER:
        _resetTimer();
        break;
      case AppConstants.TIMEOUT:
        _resetTimer();
        break;
      case AppConstants.TICK:
        _decrementTimer();
        break;
    }

    TimerStore.emitChange();

    return true;
  })
});

module.exports = TimerStore;

app-store.js

This could be named pattern-store.js, although you'd need to make some changes for it to be reusable. Specifically, I'm directly listening for the Timer's TIMEOUT action/event to trigger a pattern change. You likely don't want that dependency if you want to reuse pattern change. For example if you wanted to change the pattern by clicking a button or something.

Aside from that, I just removed all the Timer related functionality from the AppStore.

var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');

var CHANGE_EVENT = "change";

var shapes = ['C', 'A', 'G', 'E', 'D'];
var rootNotes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];

var boxShapes = require('../data/boxShapes.json');

var _pattern = _setPattern();

function _setPattern() {
  var rootNote = _getRootNote();
  var shape = _getShape();
  var boxShape = _getBoxForShape(shape);

  _pattern = {
    rootNote: rootNote,
    shape: shape,
    boxShape: boxShape
  };

  return _pattern;
}

function _getRootNote() {
  return rootNotes[Math.floor(Math.random() * rootNotes.length)];
}

function _getShape() {
  return shapes[Math.floor(Math.random() * shapes.length)];
}

function _getBoxForShape(shape) {
  return boxShapes[shape];
}

var AppStore = merge(EventEmitter.prototype, {
  emitChange: function() {
    this.emit(CHANGE_EVENT);
  },

  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  },

  getPattern: function() {
    return _pattern;
  },

  dispatcherIndex: AppDispatcher.register(function(payload) {
    var action = payload.action;

    switch(action.actionType) {
      case AppConstants.TIMEOUT:
        _setPattern();
        break;
    }

    AppStore.emitChange();

    return true;
  })
});

module.exports = AppStore;

App.jsx

Here I just added some buttons for start/stop/reset. On click, a TimerAction is dispatched. So if you clicked the "stop" button, we call TimerAction.stop()

var React = require('react');

var Headline = require('./components/Headline.jsx');
var Scale = require('./components/Scale.jsx');
var RootNote = require('./components/RootNote.jsx');
var Shape = require('./components/Shape.jsx');
var Timer = require('./components/Timer.jsx');
var TimerActions = require('./actions/timer-actions.js');


var App = React.createClass({
  render: function() {
    return (
      <div>
        <header>
          <Headline />
          <Scale />
        </header>
        <section>
          <RootNote />
          <Shape />
          <Timer />
          <button onClick={this.handleClickStart}>Start</button>
          <button onClick={this.handleClickStop}>Stop</button>
          <button onClick={this.handleClickReset}>Reset</button>
        </section>
      </div>
    );
  },
  handleClickStart: function() {
    TimerActions.start();
  },
  handleClickStop: function() {
    TimerActions.stop();
  },
  handleClickReset: function() {
    TimerActions.reset();
  }
});

module.exports = App;

Timer.jsx

One of the main changes is that we are using a TimerAction and TimerStore instead of the AppAction and AppStore that was used originally. The reason is to try to make the Timer component a bit more reusable.

The Timer has the following state:

  • status Timer status can be "start" or "stop"
  • timeLeft Time left on timer

Note that I used setTimeout instead of setInterval. I find setTimeout easier to manage.

The bulk of the logic is in the _tick method. Basically we keep calling setTimeout so long as the status is "start".

When the timer reaches zero, then we signal the timeout event. The TimerStore and AppStore are listening for this event.

  1. The TimerStore will merely reset the timer. Same the reset event.
  2. The AppStore will change the pattern.

If the timer not reached zero, we subtract one second by signaling the "tick" event.

Lastly we need to handle the case where the timer is stopped and then later started. This can be handled through the componentDidUpdate hook. This hook gets called when the component's state changes or the parent components gets re-rendered.

In the componentDidUpdate method, we make sure to start the "ticking" only if the status is "start" and the timeout identifier is undefined. We don't want multiple setTimeouts running.

var React = require('react');

var TimerActions = require('../actions/timer-actions.js');
var TimerStore = require('../stores/timer-store.js');

function getTimerState() {
  return {
    status: TimerStore.getStatus(),
    timeLeft: TimerStore.getTimeLeft()
  }
}

var Timer = React.createClass({
  _tick: function() {
    var self = this;
    this.interval = setTimeout(function() {
      if (self.state.status === 'stop') {
        self.interval = undefined;
        return;
      }

      if (self.state.timeLeft <= 0) {
        TimerActions.timeout();
      } else {
        TimerActions.tick();
      }
      self._tick();
    }, 1000);
  },
  getInitialState: function() {
    return getTimerState();
  },
  componentDidMount: function() {
    TimerStore.addChangeListener(this.handleChange);
    this._tick();
  },
  componentWillUnmount: function() {
    clearTimeout(this.interval);
    TimerStore.removeChangeListener(this.handleChange);
  },
  handleChange: function() {
    this.setState(getTimerState());
  },
  componentDidUpdate: function() {
    if (this.state.status === 'start' && this.interval === undefined) {
      this._tick();
    }
  },
  render: function() {
    return (
      <small className="timer">
        ({ this.state.timeLeft })
      </small>
    )
  }
});

module.exports = Timer;
Riddle answered 27/12, 2014 at 10:9 Comment(5)
Thank you so much for your time and effort. I will go over both implementations and try to understand what I was doing wrong, or where my thought process went wrong at least. Does your implementation follow the rules Gil Berman mentioned in his answer? I hadn't heard of action creators, nor did I know about not using setState. I still have a lot to learn/read about Flux it seems. Thank you so much!Testes
@cabaret I have not heard of Action Creators until Gil mentioned them. I need to look into that too because I know what he means about the async operations messing up data flow. My second implementation is closer to the rules that Gil mentioned (Don't store state in components).Riddle
Ha, guess we'll both have to look into that then. I'll try and implement both your solutions, see how they differ and most importantly try to understand where I went wrong. I have a feeling I don't know some crucial 'rules' of Flux; then again, the documentation on it seems to be so sparse.Testes
@cabaret I made an update to my answer. I switched the order of implementations. The "Alternate implementation" is now "Implementation A". This is my preferred solution. "Implementation B" has a big drawback in that the TimerStore and TimerAction modules are essentially singletons. This means you couldn't use multiple timer components at the same time.Riddle
Okay, sounds good. I was going over (what now is) implementation A ('alternate') and saw the comments about things not being very 'Flux', so I'm trying to figure out how to 'flux' it ;) Thanks again for your time!Testes
S
7

Don't store state in components

One of the main reasons to use flux is to centralize application state. To that end, you should avoid using a component's setState function at all. Furthermore, to the extent that components save their own state, it should only be for state data of a very fleeting nature (For example, you might set state locally on a component that indicates if a mouse is hovering).

Use Action Creators for async operations

In Flux, stores are meant to be synchronous. (Note that this is a somewhat contentious point among Flux implementations, but I definitely suggest that you make stores synchronous. Once you allow async operation in Stores, it breaks the unidirectional data flow and impairs application reasoning.). Instead, async operation should live in your Action Creator. In your code I see no mention of an Action Creator, so I suspect this might be the source of your confusion. Nevertheless, your actual Timer should live in the Action Creator. If your component needs to effect the timer, it can call a method on the Action Creator, the Action Creator can create/manage the timer, and the timer can dispatch events which will be handled by the store.

Update: Note that at the 2014 react-conf Flux panel one developer working on a large Flux application said that for that particular application they do allow async data fetching operations in the stores (GETs but not PUTs or POSTs).

Facebook's Flux Flow Chart

Seaworthy answered 27/12, 2014 at 7:35 Comment(2)
Thanks for your answer. I hadn't heard of 'action creators' before. Is this an example of them? github.com/facebook/flux/blob/master/examples/flux-todomvc/js/… It seems I have a lot of reading to do :) I will keep your answer in the back of my mind.Testes
yes, and here's another exampleSeaworthy
R
2

I would remove the timer from the store, and for now, just manage the patterns there. Your timer component would need a couple small changes:

var Timer = React.createClass({
  _tick: function() {
    if (this.state.timeLeft < 0) {
      AppActions.changePattern();
      clearInterval(this.interval);
    } else {
      this.setState({ timeLeft: this.state.timeLeft - 1 });
    }
  },
  _onChange: function() {
    // do what you want with the pattern here
    // or listen to the AppStore in another component
    // if you need this somewhere else
    var pattern = AppStore.getPattern();
  },
  getInitialState: function() {
    return { timeLeft: 60 };
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  componentDidMount: function() {
    this.interval = setInterval(this._tick, 1000);
    AppStore.addChangeListener(this._onChange);
  },
  render: function() {
    return (
      <small>
        ({ this.state.timeLeft })
      </small>
    )
  }
});
Remediable answered 22/12, 2014 at 20:0 Comment(4)
Hey, thanks for the reply. I have been trying this, but I still can't figure out how to properly implement it. I understand I can move the timeLeft stuff to the Timer component, but now I'm looking at buttons to start/pause/stop and they are in their own components (<Control />). Perhaps putting everything in the store would be better, and then trigger actions/events where necessary?Testes
Are those buttons children of this component? If so you pass in callbacks, so you can control the interval timer from this Timer component: <StopButton onClick={ this.stopTimer } />. You may need to handle the click inside the button implementation, something like this.props.onClick(e).Remediable
They aren't children of the Timer component, no. I'm close to giving up because I have no idea how to do this. Might even put a bounty on this question to see if someone can explain this to me. I really liked React/Flux to build a quick prototype, but now that I want to do more "complex" (simple timer, heh..) stuff, I'm running into a wall. Might have to read more about it.Testes
Well, like I said in my original comment, if multiple comments are working with the time interval, you should probably manage it all externally in a timer store. That store would dispatch a change event every tick and the subscribed components would do what they need with change handlers.Remediable

© 2022 - 2024 — McMap. All rights reserved.