Created
December 8, 2017 23:43
-
-
Save sompylasar/d6e3563ecc26425a44e33a317da015f1 to your computer and use it in GitHub Desktop.
Pure clock state with React, Redux, Redux-Saga. Do not use `Date.now()` or `new Date()` or `moment()` in `render()`, reading the current clock state is not pure (returns different values on each call). https://twitter.com/acdlite/status/939260579247562752
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const ACTION_CLOCK_SUBSCRIBE = 'clock/ACTION_CLOCK_SUBSCRIBE'; | |
export const ACTION_CLOCK_UNSUBSCRIBE = 'clock/ACTION_CLOCK_UNSUBSCRIBE'; | |
export const ACTION_CLOCK_UPDATED = 'clock/ACTION_CLOCK_UPDATED'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import PropTypes from 'prop-types'; | |
import { connect } from 'react-redux'; | |
import { | |
extractClockTimestamp, | |
} from './reduxClockReducer'; | |
import { | |
ACTION_CLOCK_SUBSCRIBE, | |
ACTION_CLOCK_UNSUBSCRIBE, | |
} from './reduxClockActions'; | |
import makeAction from './makeAction'; | |
/** | |
* A component that provides the clock updates in real time. | |
*/ | |
class RshClockProvider extends Component { | |
componentDidMount() { | |
const { | |
clockUpdateIntervalInMs, | |
dispatch, | |
} = this.props; | |
this._subscriptionId = Math.random(); | |
dispatch(makeAction(ACTION_CLOCK_SUBSCRIBE, { | |
subscriptionId: this._subscriptionId, | |
interval: clockUpdateIntervalInMs, | |
})); | |
} | |
componentWillUnmount() { | |
const { | |
dispatch, | |
} = this.props; | |
dispatch(makeAction(ACTION_CLOCK_UNSUBSCRIBE, { | |
subscriptionId: this._subscriptionId, | |
})); | |
} | |
render() { | |
const { | |
children, | |
clockTimestamp, | |
} = this.props; | |
const renderedChildren = children(clockTimestamp, this.props); | |
return ( renderedChildren && React.Children.only(renderedChildren) ); | |
} | |
} | |
RshClockProvider.propTypes = { | |
children: PropTypes.func.isRequired, | |
clockTimestamp: PropTypes.number.isRequired, | |
clockUpdateIntervalInMs: PropTypes.number.isRequired, | |
dispatch: PropTypes.func.isRequired, | |
}; | |
RshClockProvider.defaultProps = { | |
// NOTE(@sompylasar): By default provide a 15-second interval which is enough for one-minute-precision clock. | |
// NOTE(@sompylasar): A 500-millisecond interval should be enough for one-second-precision clock. | |
clockUpdateIntervalInMs: 15000, | |
}; | |
const RshClockProviderConnected = connect( | |
(globalState) => ({ | |
clockTimestamp: extractClockTimestamp(globalState), | |
}) | |
)(RshClockProvider); | |
export default RshClockProviderConnected; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
ACTION_CLOCK_SUBSCRIBE, | |
ACTION_CLOCK_UNSUBSCRIBE, | |
ACTION_CLOCK_UPDATED, | |
} from './reduxClockActions'; | |
// store key | |
export const STORE_KEY = 'clock'; | |
// initial state | |
const initialState = { | |
subscriptionCount: 0, | |
clockTimestamp: Date.now(), | |
}; | |
// state selectors | |
export function extractState(globalState) { | |
return (globalState[STORE_KEY] || initialState); | |
} | |
export function extractClockTimestamp(globalState) { | |
return extractState(globalState).clockTimestamp; | |
} | |
// reducer | |
export function reducer(state = initialState, action = {}) { | |
switch (action.type) { | |
case ACTION_CLOCK_SUBSCRIBE: | |
return { | |
...state, | |
subscriptionCount: state.subscriptionCount + 1, | |
}; | |
case ACTION_CLOCK_UNSUBSCRIBE: | |
return { | |
...state, | |
subscriptionCount: state.subscriptionCount - 1, | |
}; | |
case ACTION_CLOCK_UPDATED: | |
return { | |
...state, | |
clockTimestamp: action.payload.clockTimestamp, | |
}; | |
default: | |
return state; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
put, | |
take, | |
fork, | |
call, | |
} from 'redux-saga/effects'; | |
import when from 'when'; | |
import lodashReduce from 'lodash/reduce'; | |
import makeAction from './makeAction'; | |
import { | |
ACTION_CLOCK_SUBSCRIBE, | |
ACTION_CLOCK_UNSUBSCRIBE, | |
ACTION_CLOCK_UPDATED, | |
} from './reduxClockActions'; | |
const _subscriptions = {}; | |
let _intervalCurr = 0; | |
let _updaterRunning = false; | |
function makePromiseDelay(delay) { | |
return when().delay(delay); | |
} | |
function* updateClock() { | |
try { | |
_updaterRunning = true; | |
while (true) { // eslint-disable-line no-constant-condition | |
yield put(makeAction(ACTION_CLOCK_UPDATED, { | |
clockTimestamp: Date.now(), | |
})); | |
if ( _intervalCurr <= 0 ) { | |
return; | |
} | |
yield call(makePromiseDelay, _intervalCurr); | |
if ( _intervalCurr <= 0 ) { | |
return; | |
} | |
} | |
} | |
finally { | |
_updaterRunning = false; | |
} | |
} | |
function findMinInterval() { | |
return lodashReduce(_subscriptions, (accu, interval) => { | |
return (accu === 0 || interval < accu ? interval : accu); | |
}, 0); | |
} | |
function* updateInterval() { | |
// NOTE(@sompylasar): Schedule the clock updates at the highest requested frequency. | |
const intervalNext = findMinInterval(); | |
const intervalCurr = _intervalCurr; | |
_intervalCurr = intervalNext; | |
// NOTE(@sompylasar): Update the clock once if the next highest frequency is higher than the current one. | |
if ( intervalCurr > 0 && intervalNext > 0 && intervalNext < intervalCurr ) { | |
yield put(makeAction(ACTION_CLOCK_UPDATED, { | |
clockTimestamp: Date.now(), | |
})); | |
} | |
// NOTE(@sompylasar): Launch the updater if none is running. | |
if ( !_updaterRunning ) { | |
yield fork(updateClock); | |
} | |
} | |
function* watchClockSubscribe() { | |
while (true) { // eslint-disable-line no-constant-condition | |
const { payload: { subscriptionId, interval } } = yield take(ACTION_CLOCK_SUBSCRIBE); | |
if ( interval > 0 ) { | |
_subscriptions[subscriptionId] = interval; | |
yield* updateInterval(); | |
} | |
} | |
} | |
function* watchClockUnsubscribe() { | |
while (true) { // eslint-disable-line no-constant-condition | |
const { payload: { subscriptionId } } = yield take(ACTION_CLOCK_UNSUBSCRIBE); | |
delete _subscriptions[subscriptionId]; | |
yield* updateInterval(); | |
} | |
} | |
export default function* clockSaga(...args) { | |
yield put(makeAction(ACTION_CLOCK_UPDATED, { | |
clockTimestamp: Date.now(), | |
})); | |
yield [ | |
fork(watchClockSubscribe, ...args), | |
fork(watchClockUnsubscribe, ...args), | |
]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment