Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-rabbits-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'usehooks-ts': patch
---

feat(useCountdown): add `autoStart` and `onFinish` options.
16 changes: 14 additions & 2 deletions packages/usehooks-ts/src/useCountdown/useCountdown.demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,30 @@ import { useCountdown } from './useCountdown'

export default function Component() {
const [intervalValue, setIntervalValue] = useState<number>(1000)
const [message, setMessage] = useState('Running...')

const [count, { startCountdown, stopCountdown, resetCountdown }] =
useCountdown({
countStart: 60,
countStart: 10,
countStop: 0,
intervalMs: intervalValue,
autoStart: true,
onFinish: () => setMessage('Finished!'),
})

const handleChangeIntervalValue = (event: ChangeEvent<HTMLInputElement>) => {
setIntervalValue(Number(event.target.value))
}

const handleReset = () => {
setMessage('Running...')
resetCountdown()
}

return (
<div>
<p>Count: {count}</p>
<p>{message}</p>

<input
type="number"
Expand All @@ -26,7 +38,7 @@ export default function Component() {
/>
<button onClick={startCountdown}>start</button>
<button onClick={stopCountdown}>stop</button>
<button onClick={resetCountdown}>reset</button>
<button onClick={handleReset}>reset</button>
</div>
)
}
5 changes: 5 additions & 0 deletions packages/usehooks-ts/src/useCountdown/useCountdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ A simple countdown implementation. Support increment and decrement.

NEW VERSION: A simple countdown implementation. Accepts `countStop`(new), `countStart` (was `seconds`), `intervalMs`(was `interval`) and `isIncrement` as keys of the call argument. Support increment and decrement. Will stop when at `countStop`.

Also supports:

- `autoStart` to start immediately on mount.
- `onFinish` callback when the counter reaches `countStop`.

Related hooks:

- [`useBoolean()`](/react-hook/use-boolean)
Expand Down
38 changes: 38 additions & 0 deletions packages/usehooks-ts/src/useCountdown/useCountdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,42 @@ describe('useCountdown()', () => {
act(result.current[1].resetCountdown)
expect(result.current[0]).toBe(60)
})

it('should auto start when autoStart is true', () => {
const { result } = renderHook(() =>
useCountdown({ countStart: 5, countStop: 0, intervalMs: 1000, autoStart: true }),
)

act(() => {
vitest.advanceTimersByTime(2000)
})

expect(result.current[0]).toBe(3)
})

it('should call onFinish when countdown reaches countStop', () => {
const onFinish = vitest.fn()

renderHook(() =>
useCountdown({
countStart: 3,
countStop: 0,
intervalMs: 1000,
autoStart: true,
onFinish,
}),
)

act(() => {
vitest.advanceTimersByTime(3000)
})

expect(onFinish).toHaveBeenCalledTimes(1)

act(() => {
vitest.advanceTimersByTime(3000)
})

expect(onFinish).toHaveBeenCalledTimes(1)
})
})
56 changes: 38 additions & 18 deletions packages/usehooks-ts/src/useCountdown/useCountdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type CountdownOptions = {
* @default 1000
*/
intervalMs?: number

/**
* True if the countdown is increment.
* @default false
Expand All @@ -25,6 +26,17 @@ type CountdownOptions = {
* @default 0
*/
countStop?: number

/**
* Starts the countdown immediately when the hook mounts.
* @default false
*/
autoStart?: boolean

/**
* Callback fired once the countdown reaches `countStop`.
*/
onFinish?: () => void
}

/** The countdown's controllers. */
Expand All @@ -49,6 +61,8 @@ type CountdownControllers = {
* countStart: 10,
* intervalMs: 1000,
* isIncrement: false,
* autoStart: true,
* onFinish: () => console.log('Done!'),
* });
* ```
*/
Expand All @@ -57,13 +71,10 @@ export function useCountdown({
countStop = 0,
intervalMs = 1000,
isIncrement = false,
autoStart = false,
onFinish,
}: CountdownOptions): [number, CountdownControllers] {
const {
count,
increment,
decrement,
reset: resetCounter,
} = useCounter(countStart)
const { count, reset: resetCounter, setCount } = useCounter(countStart)

/*
* Note: used to control the useInterval
Expand All @@ -75,7 +86,7 @@ export function useCountdown({
value: isCountdownRunning,
setTrue: startCountdown,
setFalse: stopCountdown,
} = useBoolean(false)
} = useBoolean(autoStart)

// Will set running false and reset the seconds to initial value.
const resetCountdown = useCallback(() => {
Expand All @@ -84,17 +95,26 @@ export function useCountdown({
}, [stopCountdown, resetCounter])

const countdownCallback = useCallback(() => {
if (count === countStop) {
stopCountdown()
return
}

if (isIncrement) {
increment()
} else {
decrement()
}
}, [count, countStop, decrement, increment, isIncrement, stopCountdown])
setCount(previousCount => {
if (previousCount === countStop) {
stopCountdown()
onFinish?.()
return previousCount
}

const nextCount = isIncrement ? previousCount + 1 : previousCount - 1
const willReachStop = isIncrement
? nextCount >= countStop
: nextCount <= countStop

if (willReachStop) {
stopCountdown()
onFinish?.()
}

return nextCount
})
}, [countStop, isIncrement, onFinish, setCount, stopCountdown])

useInterval(countdownCallback, isCountdownRunning ? intervalMs : null)

Expand Down