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.
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 connect
ed 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 TICK
s 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
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
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
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() {...}
}