function createStore(initialState, reducers) {
let state = Object.freeze(initialState)
const reducerMap = reducers.reduce(
(p, { type, reducer }) => (p[type] ? p[type].push(reducer) : (p[type] = [reducer]), p), {})
function dispatch(action, ...args) {
state = reducerMap[action].reduce((p, c) => c(p, ...args), state)
}
return {
dispatch,
getState() {
return state
}
}
}
const toTwoDigitString = (n) => (n + "").padStart(2, "0")
const formatForDisplay = (seconds) =>
`${toTwoDigitString(~~(seconds / 60))}:${toTwoDigitString(
~~(seconds % 60)
)} minutes`
const tick = (store) =>
store.getState().elapsedSeconds >= store.getState().durationSeconds
? clearInterval(store.getState().timerId)
: store.dispatch(ACTION_TICK)
const startTimer = (store) => {
if (store.getState().timerId !== null) return
const timerId = setInterval(() => tick(store), 1000)
store.dispatch(ACTION_START, timerId)
}
const resetTimer = (store) => {
clearInterval(store.getState().timerId)
store.dispatch(ACTION_RESET)
}
const render = ({ displayEl, durationSeconds, elapsedSeconds }) =>
(displayEl.innerHTML = formatForDisplay(durationSeconds - elapsedSeconds))
const ACTION_START = "ACTION_START"
const ACTION_RESET = "ACTION_RESET"
const ACTION_TICK = "ACTION_TICK"
const reducerStart = {
type: ACTION_START,
reducer(state, timerId) {
return {
...state,
timerId
}
}
}
const reducerReset = {
type: ACTION_RESET,
reducer(state) {
return {
...state,
timerId: null,
elapsedSeconds: 0
}
}
}
const reducerTick = {
type: ACTION_TICK,
reducer: (state) => {
return {
...state,
elapsedSeconds: ++state.elapsedSeconds
}
}
}
function createTimer({ selector, durationSeconds }) {
const timerEl = document.querySelector(selector)
const initialState = {
timerId: null,
displayEl: timerEl.querySelector("#time"),
durationSeconds,
elapsedSeconds: 0
}
const store = createStore(initialState, [
reducerStart,
reducerReset,
reducerTick
])
const start = () => startTimer(store)
const reset = () => resetTimer(store)
timerEl.querySelector("button#start").addEventListener("click", start)
timerEl.querySelector("button#reset").addEventListener("click", reset)
const previousState = null
;(function renderLoop() {
if (previousState !== store.getState()) render(store.getState())
requestAnimationFrame(renderLoop)
})()
}
createTimer({ selector: "#timer", durationSeconds: 2 * 5 })
* {
font-family: sans-serif;
color: #dc1a1a !important;
font-size: 1.1rem;
padding: 10px;
margin: 10px;
}
<div id="timer">
<span id="time">The time is:</span>
<button id="start">start</button>
<button id="reset">reset</button>
</div>