Работа с таймером в 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 и каким-то образом передать его моему компоненту?

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


person Joris Ooms    schedule 22.12.2014    source источник
comment
Вам нужно иметь возможность восстановить оставшееся время на таймере? Скажем, если хранилище было сохранено на сервере, должно ли обновление страницы отслеживать оставшееся время? Если да, то timeLeft, вероятно, тоже принадлежит магазину.   -  person Ross Allen    schedule 22.12.2014
comment
Я ничего не сохраняю на сервере. Единственное, что я хочу, чтобы он мог запускать/приостанавливать/останавливать таймер. При обновлении он должен снова начинаться с 60 секунд. Это мой магазин в том виде, в каком он у меня сейчас: pastebin.com/MwV6cRbe   -  person Joris Ooms    schedule 22.12.2014
comment
Если никакому другому компоненту не нужен доступ к timeLeft, я бы сохранил все это внутри вашего компонента Timer. Тогда вы могли бы просто начать и остановить интервал. В противном случае вам нужно контролировать интервал в хранилище и отправлять события изменения.   -  person Shawn    schedule 22.12.2014
comment
@Shawn Когда мой таймер запускается, я хочу отправить событие CHANGE_PATTERN, поэтому могу ли я просто обработать запуск и остановку в своем компоненте таймера (а также переместить туда timeLeft из хранилища), а затем выполнять AppStore.changePattern() всякий раз, когда мой таймер начинается? Или это идет вразрез со всем однонаправленным потоком Flux? Немного запутался в том, как правильно решить эту проблему. Спасибо!   -  person Joris Ooms    schedule 22.12.2014
comment
Не уверен, что это способ Flux сделать это, но, возможно, предоставить состояние запуска/остановки/паузы/сброса, которым управляет корневое приложение, и передать его таймеру в качестве опоры. Затем вы можете передать событие щелчка компоненту кнопки из корневого приложения. Когда кнопка нажата, обновите состояние запуска/остановки/паузы приложения, которое затем запускает обновление рендеринга, при котором новое состояние запуска/остановки/паузы передается таймеру в качестве реквизита. Просто размышлял в основном.   -  person Gohn67    schedule 25.12.2014


Ответы (3)


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

Я фактически закончил с двумя реализациями. Я назову их Реализация А и Реализация Б.

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

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

Вот краткие описания обеих реализаций:

Реализация А

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

Реализация Б

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

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


|

|

Реализация А

|

|

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

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

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

  • app-constants.js
  • приложение-actions.js
  • app-store.js
  • App.jsx
  • Таймер.jsx

app-constants.js

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

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

app-actions.js

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

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

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

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 внесены многочисленные изменения, в частности, мы перенесли состояние в компонент приложения из компонента таймера. Снова подробные комментарии в коде.

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;

Таймер.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;

|

|

Реализация Б

|

|

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

  • app-constants.js
  • timer-actions.js (новое)
  • timer-store.js (новое)
  • app-store.js
  • App.jsx
  • Таймер.jsx

app-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

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

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

Я отделил элементы таймера от файла AppStore. Это сделано для того, чтобы сделать компонент Timer более удобным для повторного использования.

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

  • статус таймера — может быть "запуск" или "останов".
  • время осталось — время, оставшееся до окончания таймера.

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

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

Вот код:

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

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

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

Здесь я просто добавил несколько кнопок для запуска/остановки/сброса. При щелчке отправляется 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;

Таймер.jsx

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

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

  • статус Статус таймера может быть "запущен" или "остановлен".
  • timeLeft Оставшееся время таймера

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

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

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

  1. TimerStore просто сбросит таймер. То же самое событие сброса.
  2. AppStore изменит шаблон.

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

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

В методе componentDidUpdate мы обязательно запускаем «тикание» только в том случае, если статус «старт» и идентификатор тайм-аута не определен. Мы не хотим запускать несколько 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;
person Gohn67    schedule 27.12.2014
comment
Большое спасибо за ваше время и усилия. Я пройдусь по обеим реализациям и попытаюсь понять, что я делал не так, или, по крайней мере, где мой мыслительный процесс пошел не так. Соответствует ли ваша реализация правилам, упомянутым Гил Берманом в его ответе? Я не слышал о создателях действий и не знал о том, что не использую setState. Кажется, мне еще многое предстоит узнать/прочитать о Flux. Большое спасибо! - person Joris Ooms; 27.12.2014
comment
@cabaret Я не слышал о создателях боевиков, пока Гил не упомянул их. Мне нужно изучить это тоже, потому что я знаю, что он имеет в виду, говоря об асинхронных операциях, которые портят поток данных. Моя вторая реализация ближе к правилам, которые упомянул Гил (не хранить состояние в компонентах). - person Gohn67; 27.12.2014
comment
Ха, полагаю, тогда нам обоим придется разобраться в этом. Я попробую реализовать оба ваших решения, посмотрю, чем они отличаются, и, самое главное, попытаюсь понять, где я ошибся. У меня такое чувство, что я не знаю некоторых важных «правил» Flux; опять же, документация по нему кажется такой скудной. - person Joris Ooms; 27.12.2014
comment
@cabaret Я обновил свой ответ. Я поменял порядок реализации. Альтернативная реализация теперь называется реализацией A. Это мое предпочтительное решение. Реализация B имеет большой недостаток, заключающийся в том, что модули TimerStore и TimerAction по существу являются синглтонами. Это означает, что вы не можете использовать несколько компонентов таймера одновременно. - person Gohn67; 27.12.2014
comment
Ладно, звучит хорошо. Я просматривал (то, что сейчас есть) реализацию A («альтернативный») и видел комментарии о том, что вещи не очень «потоковые», поэтому я пытаюсь понять, как это «изменить»;) Еще раз спасибо за ваше время. ! - person Joris Ooms; 27.12.2014

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

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

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

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

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

Схема потока Facebook

person Gil Birman    schedule 27.12.2014
comment
Спасибо за Ваш ответ. Раньше я не слышал о «создателях действий». Это их пример? github.com/facebook/flux/ blob/master/examples/flux-todomvc/js/ Кажется, мне нужно много читать :) Я буду держать ваш ответ в памяти. - person Joris Ooms; 27.12.2014

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

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>
    )
  }
});
person Shawn    schedule 22.12.2014
comment
Эй, спасибо за ответ. Я пробовал это, но я до сих пор не могу понять, как это правильно реализовать. Я понимаю, что могу переместить материал timeLeft в компонент Timer, но теперь я смотрю на кнопки для запуска/паузы/остановки, и они находятся в своих собственных компонентах (‹Control /›). Возможно, было бы лучше поместить все в магазин, а затем запускать действия/события там, где это необходимо? - person Joris Ooms; 23.12.2014
comment
Являются ли эти кнопки дочерними элементами этого компонента? Если это так, вы передаете обратные вызовы, чтобы вы могли управлять интервальным таймером из этого компонента Timer: ‹StopButton onClick={ this.stopTimer } /›. Вам может понадобиться обработать щелчок внутри реализации кнопки, что-то вроде this.props.onClick(e). - person Shawn; 23.12.2014
comment
Они не являются потомками компонента Timer, нет. Я близок к тому, чтобы сдаться, потому что понятия не имею, как это сделать. Мог бы даже назначить награду за этот вопрос, чтобы посмотреть, сможет ли кто-нибудь объяснить это мне. Мне очень понравился React/Flux для создания быстрого прототипа, но теперь, когда я хочу делать более сложные вещи (простой таймер, хех..), я упираюсь в стену. Возможно, придется прочитать больше об этом. - person Joris Ooms; 23.12.2014
comment
Ну, как я сказал в своем первоначальном комментарии, если несколько комментариев работают с временным интервалом, вам, вероятно, следует управлять всем этим извне в хранилище таймеров. Это хранилище будет отправлять событие изменения каждый тик, а подписанные компоненты будут делать то, что им нужно, с обработчиками изменений. - person Shawn; 23.12.2014