web-dev-qa-db-fra.com

Créer un chronomètre avec redux

J'ai essayé de faire un chronomètre dans réagir et redux. J'ai eu du mal à comprendre comment concevoir une telle chose dans Redux. 

La première chose qui me vint à l’esprit était d’avoir une action START_TIMER qui définirait la valeur offset initiale. Juste après cela, j'utilise setInterval pour déclencher une action TICK encore et encore, qui calcule le temps écoulé avec le décalage, l'ajoute à l'heure actuelle, puis met à jour la offset.

Cette approche semble fonctionner, mais je ne suis pas sûre de savoir comment dégager l'intervalle pour l'arrêter. En outre, il semble que cette conception est médiocre et qu'il existe probablement une meilleure façon de le faire.

Voici un JSFiddle complet avec la fonctionnalité START_TIMER qui fonctionne. Si vous voulez juste voir à quoi ressemble mon réducteur en ce moment, le voici:

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

J'apprécierais vraiment toute aide.

14
saadq

Je recommanderais probablement de procéder différemment: enregistrez uniquement l'état nécessaire pour calculer le temps écoulé dans le magasin et laissez les composants définir leur intervalle propre aussi souvent qu'ils souhaitent mettre à jour l'affichage.

Cela permet de réduire au minimum les répartitions d'actions. Seules les actions permettant de démarrer et d'arrêter (et de réinitialiser) le temporisateur sont envoyées. N'oubliez pas que vous renvoyez un nouvel objet d'état à chaque fois vous envoyez une action, puis chaque composant connected effectue un nouveau rendu (même s'il utilise des optimisations pour éviter un trop grand nombre de rendus dans les composants enveloppés). En outre, de nombreuses actions envoyées peuvent rendre difficile le débogage des modifications d'état des applications, car vous devez gérer toutes les TICKs aux côtés des autres actions.

Voici un exemple:

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

Notez que les créateurs et les réducteurs d'action traitent uniquement avec des valeurs primitives et n'utilisent aucune sorte d'intervalle ni de type d'action TICK. Désormais, un composant peut facilement s’abonner à ces données et se mettre à jour aussi souvent qu’il le souhaite:

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

Vous pouvez même afficher plusieurs minuteries sur les mêmes données avec une fréquence de mise à jour différente:

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

Vous pouvez voir un travail JSBin avec cette implémentation ici: https://jsbin.com/dupeji/12/edit?js,output

38
Michelle Tilley

Si vous envisagez d’utiliser cette application dans une application plus grande, j’utiliserais requestAnimationFrame au lieu d’une setInterval pour les problèmes de performances. Pendant que vous affichez les millisecondes, vous remarquerez ceci sur les appareils mobiles, pas tellement sur les navigateurs de bureau.

JSFiddle mis à jour

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

9
andykenward

Vous souhaitez utiliser la fonction clearInterval qui prend le résultat d'un appel à setInterval (un identificateur unique) et empêche cet intervalle de s'exécuter davantage.

Ainsi, plutôt que de déclarer une setInterval dans start(), transmettez-la au réducteur afin qu'il puisse stocker son ID dans l'état

Pass interval à dispatch en tant que membre de l'objet action

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

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

Store interval sur le nouvel état dans le réducteur d'action START_TIMER

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

______

Rendu du composant selon interval

Transmettez interval en tant que propriété du composant:

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

Nous pouvons ensuite inspecter l'état dans le composant out pour le rendre en fonction de la propriété interval ou non:

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

______

Arrêt du chronomètre

Pour arrêter le chronomètre, effacez l’intervalle en utilisant clearInterval et appliquez simplement à nouveau initialState:

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

______

JSFiddle mis à jour

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

5
sdgluck

Semblable à la réponse d’andykenward, j’utiliserais requestAnimationFrame pour améliorer les performances, car la cadence de la plupart des appareils n’est que d’environ 60 images par seconde. Cependant, je mettrais le moins possible dans Redux. Si vous avez juste besoin de l'intervalle pour distribuer les événements, vous pouvez le faire au niveau des composants plutôt que dans Redux. Voir le commentaire de Dan Abramov dans cette réponse .

Vous trouverez ci-dessous un exemple de composant Minuterie de compte à rebours affichant un compte à rebours et effectuant une opération après son expiration. Dans start, tick ou stop, vous pouvez envoyer les événements que vous devez déclencher dans Redux. Je ne monte ce composant que lorsque le minuteur doit démarrer. 

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() {...}
}
0
Sia