web-dev-qa-db-ja.com

reduxでストップウォッチを作成する

私は反応と還元でストップウォッチを作ろうとしています。私はそのようなものをどうやってデザインするかを理解するのに苦労しています。

最初に頭に浮かんだのは、START_TIMERアクションは、初期のoffset値を設定します。その直後、setIntervalを使用してTICKアクションを何度も起動し、オフセットを使用して経過時間を計算し、それを現在の時刻に追加して、 offset

このアプローチは機能しているようですが、それを停止する間隔をどのようにクリアするかはわかりません。また、このデザインは貧弱で、おそらくもっと良い方法があるようです。

ここに完全な JSFiddle があり、START_TIMER機能しています。私のレデューサーが今どのように見えるかを確認したい場合は、次のとおりです。

const initialState = {
  isOn: false,
  time: 0
};

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

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

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

    default: 
      return state;
  }
}

私は本当に助けていただければ幸いです。

16
saadq

私はおそらくこれを別の方法で行うことをお勧めします:経過時間を計算するために必要な状態のみをストアに保存し、コンポーネントにown間隔を設定させます多くの場合、表示を更新したいと考えています。

これにより、アクションのディスパッチが最小限に抑えられ、タイマーを開始および停止(およびリセット)するアクションのみがディスパッチされます。新しい状態オブジェクトを返す毎回アクションをディスパッチし、各connectedコンポーネントが再レンダリングされることを忘れないでください(ただし、ラップされたコンポーネント内での再レンダリングが多すぎることを避けるために最適化を使用しています)。さらに、他のアクションと一緒にすべてのTICKsを処理する必要があるため、多くの多くのアクションディスパッチはアプリの状態変化のデバッグを困難にする可能性があります。

次に例を示します。

// Action Creators

function startTimer(baseTime = 0) {
  return {
    type: "START_TIMER",
    baseTime: baseTime,
    now: new Date().getTime()
  };
}

function stopTimer() {
  return {
    type: "STOP_TIMER",
    now: new Date().getTime()
  };
}

function resetTimer() {
  return {
    type: "RESET_TIMER",
    now: new Date().getTime()
  }
}


// Reducer / Store

const initialState = {
  startedAt: undefined,
  stoppedAt: undefined,
  baseTime: undefined
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case "RESET_TIMER":
      return {
        ...state,
        baseTime: 0,
        startedAt: state.startedAt ? action.now : undefined,
        stoppedAt: state.stoppedAt ? action.now : undefined
      };
    case "START_TIMER":
      return {
        ...state,
        baseTime: action.baseTime,
        startedAt: action.now,
        stoppedAt: undefined
      };
    case "STOP_TIMER":
      return {
        ...state,
        stoppedAt: action.now
      }
    default:
      return state;
  }
}

const store = createStore(reducer);

アクションの作成者とレデューサーはプリミティブ値のみを扱い、いかなる種類の間隔やTICKアクションタイプも使用しないことに注意してください。これで、コンポーネントはこのデータを簡単にサブスクライブして、必要な頻度で更新できます。

// Helper function that takes store state
// and returns the current elapsed time
function getElapsedTime(baseTime, startedAt, stoppedAt = new Date().getTime()) {
  if (!startedAt) {
    return 0;
  } else {
    return stoppedAt - startedAt + baseTime;
  }
}

class Timer extends React.Component {
  componentDidMount() {
    this.interval = setInterval(this.forceUpdate.bind(this), this.props.updateInterval || 33);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    const { baseTime, startedAt, stoppedAt } = this.props;
    const elapsed = getElapsedTime(baseTime, startedAt, stoppedAt);

    return (
      <div>
        <div>Time: {elapsed}</div>
        <div>
          <button onClick={() => this.props.startTimer(elapsed)}>Start</button>
          <button onClick={() => this.props.stopTimer()}>Stop</button>
          <button onClick={() => this.props.resetTimer()}>Reset</button>
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  const { baseTime, startedAt, stoppedAt } = state;
  return { baseTime, startedAt, stoppedAt };
}

Timer = ReactRedux.connect(mapStateToProps, { startTimer, stopTimer, resetTimer })(Timer);

同じデータに異なる更新頻度で複数のタイマーを表示することもできます。

class Application extends React.Component {
  render() {
    return (
      <div>
        <Timer updateInterval={33} />
        <Timer updateInterval={1000} />
      </div>
    );
  }
}

この実装で working JSBin をここで確認できます: https://jsbin.com/dupeji/12/edit?js,output

42
Michelle Tilley

大きなアプリでこれを使用する場合、パフォーマンスの問題のためにrequestAnimationFrameではなくsetIntervalを使用します。ミリ秒を表示しているので、モバイルデバイスではデスクトップブラウザーではそれほど気になりません。

更新されたJSFiddle

https://jsfiddle.net/andykenward/9y1jjsuz

10
andykenward

clearInterval(一意の識別子)への呼び出しの結果を受け取り、その間隔がそれ以上実行されないようにする setInterval 関数を使用します。

そのため、start()内でsetIntervalを宣言する代わりに、それをレデューサーに渡して、IDを状態に保存できるようにします。

_intervalをアクションオブジェクトのメンバーとしてディスパッチャーに渡します

start() {
  const interval = setInterval(() => {
    store.dispatch({
      type: 'TICK',
      time: Date.now()
    });
  });

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

START_TIMERアクションレデューサー内の新しい状態にintervalを保存します

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

______

intervalに従ってコンポーネントをレンダリングする

コンポーネントのプロパティとしてintervalを渡します。

const render = () => {
  ReactDOM.render(
    <Timer 
      time={store.getState().time}
      isOn={store.getState().isOn}
      interval={store.getState().interval}
    />,
    document.getElementById('app')
  );
}

次に、outコンポーネント内の状態を調べて、プロパティintervalがあるかどうかに応じてレンダリングします。

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

______

タイマーの停止

タイマーを停止するには、clearIntervalを使用して間隔をクリアし、initialStateを再度適用するだけです。

case 'STOP_TIMER':
  clearInterval(state.interval);
  return {
    ...initialState
  };

______

更新されたJSFiddle

https://jsfiddle.net/8z16xwd2/2/

5
sdgluck

Andykenwardの回答と同様に、私はrequestAnimationFrameを使用してパフォーマンスを向上させます。ほとんどのデバイスのフレームレートは1秒あたり約60フレームしかないからです。しかし、私はできるだけReduxに入れません。イベントをディスパッチする間隔だけが必要な場合は、Reduxではなくコンポーネントレベルですべて実行できます。 この答え のDan Abramovのコメントを参照してください。

以下は、カウントダウンクロックを表示し、期限が切れたときに何かを行うカウントダウンタイマーコンポーネントの例です。 starttick、またはstop内で、Reduxで起動する必要があるイベントをディスパッチできます。タイマーを開始するときにのみ、このコンポーネントをマウントします。

class Timer extends Component {
  constructor(props) {
    super(props)
    // here, getTimeRemaining is a helper function that returns an 
    // object with { total, seconds, minutes, hours, days }
    this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
  }

  // Wait until the component has mounted to start the animation frame
  componentDidMount() {
    this.start()
  }

  // Clean up by cancelling any animation frame previously scheduled
  componentWillUnmount() {
    this.stop()
  }

  start = () => {
    this.frameId = requestAnimationFrame(this.tick)
  }

  tick = () => {
    const timeLeft = getTimeRemaining(this.props.expiresAt)
    if (timeLeft.total <= 0) {
      this.stop()
      // dispatch any other actions to do on expiration
    } else {
      // dispatch anything that might need to be done on every tick
      this.setState(
        { timeLeft },
        () => this.frameId = requestAnimationFrame(this.tick)
      )
    }
  }

  stop = () => {
    cancelAnimationFrame(this.frameId)
  }

  render() {...}
}
1
Sia