Обработка таймера в режиме React/Flux

Я работаю над приложением, где я хочу, чтобы таймер обратного отсчета от, скажем, от 60 секунд до 0, а затем менял какой-то контент, после чего таймер снова запускается на 60.

Я реализовал это в React и Flux, но поскольку я новичок в этом, у меня все еще возникают проблемы.

Теперь я хочу добавить кнопку запуска/остановки для таймера. Я не уверен, где поставить/обработать состояние таймера.

У меня есть компонент Timer.jsx, который выглядит так:

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;

Он извлекает длительность обратного отсчета из хранилища, где у меня просто есть:

var _timeLeft = 60;

Теперь, когда я хочу реализовать кнопку запуска/остановки, мне кажется, что я должен также реализовать это через Flux Actions, правильно? Поэтому я думал о том, что в моем магазине есть что-то подобное:

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

Однако, поскольку мой компонент Timer в настоящее время обрабатывает setInterval, я не знаю, как работать с моими START/STOP_TIMER событиями. Должен ли я перемещать материал SetInterval из компонента Timer в Store и каким-то образом передавать его на мой компонент?

Полный код можно найти здесь.

Ответы

Ответ 1

Я закончил загрузку кода и реализацию функции start/stop/ reset, которую вы хотели. Я думаю, что, вероятно, лучший способ объяснить вещи - показать код, который вы можете запустить и протестировать вместе с некоторыми комментариями.

На самом деле я оказался с двумя реализациями. Я буду называть их Реализация A и Реализация B.

Я подумал, что было бы интересно показать обе реализации. Надеюсь, это не вызовет слишком много путаницы.

Для записи реализация A - лучшая версия.

Ниже приведены краткие описания обеих реализаций:

Реализация A

Эта версия отслеживает состояние на уровне компонента приложения. Таймер управляется передачей props компоненту Timer. Компонент таймера, тем не менее, отслеживает свое собственное время влево.

Реализация B

Эта версия отслеживает состояние таймера на уровне компонента Timer с помощью модуля TimerStore и TimerAction для управления состоянием и событиями компонента.

Большой (и, вероятно, фатальный) недостаток реализации B заключается в том, что у вас может быть только один компонент Timer. Это связано с тем, что модули TimerStore и TimerAction по существу являются Singletons.


|

|

Реализация A

|

|

Эта версия отслеживает состояние на уровне компонента приложения. Большинство комментариев здесь приведены в коде для этой версии.

Таймер управляется передачей props в таймер.

Список изменений кода для этой реализации:

  • приложение-constants.js
  • приложение-actions.js
  • приложение-store.js
  • App.jsx
  • Timer.jsx

приложение-constants.js

Здесь я просто добавил константу для повторного запуска таймера.

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

приложение-actions.js

Я только что добавил метод отправки для обработки действия таймера reset.

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;

приложение-store.js

Здесь кое-что немного изменилось. Я добавил подробные комментарии, в которых я внес изменения.

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

В App.jsx есть множество изменений, в частности, мы переместили состояние в компонент App из компонента таймера. Снова подробные комментарии в коде.

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

Timer имеет множество изменений, так как я удалил зависимости AppStore и AppActions, чтобы сделать компонент Timer более многократно использованным. Подробные комментарии приведены в коде.

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;

|

|

Реализация B

|

|

Список изменений кода:

  • приложение-constants.js
  • timer-actions.js(новый)
  • timer-store.js(новый)
  • приложение-store.js
  • App.jsx
  • Timer.jsx

приложение-constants.js

Они, вероятно, должны идти в файле с именем timer-constants.js, поскольку они имеют дело с компонентом Timer.

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

Таймер-actions.js

Этот модуль не требует пояснений. Я добавил три события - тайм-аут, галочку и reset. Подробнее см. В коде.

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

Таймер-store.js

Я выделил материал таймера из AppStore. Это должно сделать компонент Timer немного более многоразовым.

Хранилище таймеров отслеживает следующее состояние:

  • статус таймера - может быть "start" или "stop"
  • оставшееся время - время, оставшееся на таймере

Хранилище таймеров обрабатывает следующие события:

  • Событие запуска таймера устанавливает время запуска таймера.
  • Событие остановки таймера устанавливает статус таймера для остановки.
  • Событие галочки уменьшает время, оставшееся до 1
  • Событие таймера reset устанавливает время, оставшееся до значения по умолчанию, и устанавливает статус таймера для запуска

Вот код:

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;

приложение-store.js

Это можно назвать pattern-store.js, хотя вам нужно будет внести некоторые изменения для его повторного использования. В частности, я непосредственно прислушиваюсь к действию/событию Timer TIMEOUT, чтобы вызвать изменение шаблона. Вероятно, вам не нужна эта зависимость, если вы хотите повторно использовать изменение шаблона. Например, если вы хотите изменить шаблон, нажав кнопку или что-то в этом роде.

Кроме того, я просто удалил все связанные с Timer функции из 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

Здесь я просто добавил несколько кнопок для начала/остановки/ reset. По щелчку отправляется TimerAction. Поэтому, если вы нажали кнопку "Стоп", мы вызываем 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

Одно из главных изменений заключается в том, что мы используем TimerAction и TimerStore вместо AppAction и AppStore, которые использовались первоначально. Причина заключается в том, чтобы попытаться сделать компонент Timer немного более многоразовым.

Таймер имеет следующее состояние:

  • статус Статус таймера может быть "start" или "stop"
  • timeLeft Время, оставшееся на таймере

Обратите внимание, что я использовал setTimeout вместо setInterval. Я считаю, что setTimeout легче управлять.

Основная часть логики находится в методе _tick. В основном мы продолжаем звонить setTimeout, пока статус "начинается".

Когда таймер достигает нуля, мы сигнализируем событие TIMEOUT. TimerStore и AppStore прослушивают это событие.

  • TimerStore будет просто reset таймером. То же самое событие reset.
  • AppStore изменит шаблон.

Если таймер не достиг нулевого значения, мы вычитаем одну секунду, сигнализируя о событии "tick".

Наконец, нам нужно обработать случай, когда таймер остановлен, а затем запущен позже. Это можно обработать с помощью крюка componentDidUpdate. Этот крючок вызывается, когда изменяется состояние компонента или регенерируются родительские компоненты.

В методе componentDidUpdate мы обязательно начинаем "тикать", только если статус "старт", а идентификатор таймаута - undefined. Мы не хотим запускать несколько запусков setTimeouts.

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;

Ответ 2

Не сохранять состояние в компонентах

Одной из основных причин использования потока является централизовать состояние приложения. Для этого вам следует избежать использования компонента setState вообще. Кроме того, в той мере, в какой компоненты сохраняют свое собственное состояние, это должно быть только для данных состояния очень мимолетного характера (например, вы можете установить состояние локально на компоненте, указывающем на то, что мышь витает).

Использовать Action Creators для асинхронных операций

В Flux хранилища должны быть синхронными. (Обратите внимание, что это довольно спорный вопрос среди реализаций Flux, но я определенно предлагаю вам сделать синхронизацию магазинов. После того, как вы разрешаете операцию async в Stores, она прерывает однонаправленный поток данных и ухудшает рассуждения приложений.). Вместо этого операция async должна жить в Action Creator. В вашем коде я не вижу упоминания о Создателе Action, поэтому я подозреваю, что это может быть источником вашей путаницы. Тем не менее, ваш фактический Таймер должен жить в Action Creator. Если вашему компоненту необходимо выполнить таймер, он может вызывать метод в Action Creator, Action Creator может создавать/управлять таймером, а таймер может отправлять события, которые будут обрабатываться хранилищем.

Обновление. Обратите внимание, что на панели реагирования Reply-conf 2014 один разработчик, работающий над большим приложением Flux, сказал, что для этого конкретного приложения они разрешают асинхронные операции выборки данных в магазинах (GET, но не PUT или POST).

Facebook's Flux Flow Chart

Ответ 3

Я бы удалил таймер из магазина, и на данный момент просто управляем шаблонами. Вашему компоненту таймера потребуется несколько небольших изменений:

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