-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The hook can be used to run a function after a specific amount of time for example for doing a reload of data.
- Loading branch information
1 parent
3371ec5
commit e03032b
Showing
2 changed files
with
165 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* SPDX-FileCopyrightText: 2024 Greenbone AG | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
import {useState} from 'react'; | ||
|
||
import {describe, test, expect, testing} from '@gsa/testing'; | ||
|
||
import {act, fireEvent, render, screen} from 'web/utils/testing'; | ||
|
||
import useTiming from '../useTiming'; | ||
|
||
const TestComponent = () => { | ||
const [value, setValue] = useState(0); | ||
const updateValue = () => setValue(1); | ||
// eslint-disable-next-line no-unused-vars | ||
const [startTimer, clearTimer, isRunning] = useTiming(updateValue, 900); | ||
return ( | ||
<> | ||
<button onClick={startTimer} data-testid="startTimer"></button> | ||
<span data-testid="value">{value}</span> | ||
<span data-testid="isRunning">{'' + isRunning}</span> | ||
</> | ||
); | ||
}; | ||
|
||
const runTimers = async () => { | ||
await act(async () => { | ||
await testing.runAllTimersAsync(); | ||
}); | ||
}; | ||
|
||
describe('useTiming', () => { | ||
test('should start a timer', async () => { | ||
testing.useFakeTimers(); | ||
|
||
render(<TestComponent />); | ||
|
||
const value = screen.getByTestId('value'); | ||
const isRunning = screen.getByTestId('isRunning'); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('false'); | ||
|
||
fireEvent.click(screen.getByTestId('startTimer')); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('true'); | ||
|
||
await runTimers(); | ||
|
||
expect(screen.getByTestId('value')).toHaveTextContent('1'); | ||
expect(screen.getByTestId('isRunning')).toHaveTextContent('false'); | ||
}); | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/* SPDX-FileCopyrightText: 2024 Greenbone AG | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
import {useEffect, useCallback, useState} from 'react'; | ||
|
||
import logger from 'gmp/log'; | ||
|
||
import {hasValue, isFunction, isDefined} from 'gmp/utils/identity'; | ||
|
||
import useInstanceVariable from './useInstanceVariable'; | ||
|
||
const log = logger.getLogger('web.hooks.useTiming'); | ||
|
||
/** | ||
* Hook to start a timer that calls a function after a given timeout | ||
* | ||
* @param {Function} doFunc The function to call | ||
* @param {Number|Function} timeout The timeout in milliseconds or a function that returns the timeout | ||
* @returns Array of startTimer function, clearTimer function and boolean isRunning | ||
*/ | ||
const useTiming = (doFunc, timeout) => { | ||
const timer = useInstanceVariable({}); | ||
const [timerId, setTimerId] = useState(); // store timerId in state too to trigger re-render if it changes | ||
const isRunning = !!timerId; | ||
timer.doFunc = doFunc; // always use the latest version of the function | ||
|
||
const updateTimerId = useCallback( | ||
newTimerId => { | ||
timer.timerId = newTimerId; | ||
setTimerId(newTimerId); | ||
}, | ||
[timer], | ||
); | ||
|
||
const startTimer = useCallback(() => { | ||
if (hasValue(timer.timerId)) { | ||
log.debug('Not starting timer. A timer is already running.', { | ||
timer: timer.timerId, | ||
}); | ||
return; | ||
} | ||
|
||
const timeoutValue = isFunction(timeout) ? timeout() : timeout; | ||
|
||
if (!hasValue(timeoutValue) || timeoutValue < 0) { | ||
log.debug('Not starting timer because timeout value was', timeoutValue); | ||
return; | ||
} | ||
|
||
updateTimerId( | ||
setTimeout(() => { | ||
log.debug('Timer with id', timer.timerId, 'fired.'); | ||
|
||
const promise = timer.doFunc(); | ||
|
||
if (isDefined(promise?.then)) { | ||
promise | ||
.then(() => { | ||
updateTimerId(undefined); | ||
timer.startTimer(); | ||
}) | ||
.catch(() => { | ||
updateTimerId(undefined); | ||
}); | ||
} else { | ||
updateTimerId(undefined); | ||
} | ||
}, timeoutValue), | ||
); | ||
|
||
log.debug( | ||
'Started timer with id', | ||
timer.timerId, | ||
'and timeout of', | ||
timeoutValue, | ||
'milliseconds', | ||
); | ||
}, [timeout, timer, updateTimerId]); | ||
|
||
const clearTimer = useCallback(() => { | ||
if (hasValue(timer.timerId)) { | ||
log.debug('Clearing timer with id', timer.timerId); | ||
|
||
clearTimeout(timer.timerId); | ||
updateTimerId(undefined); | ||
} | ||
}, [timer, updateTimerId]); | ||
|
||
useEffect(() => { | ||
// put starTimer func into timer ref to allow referencing the NEWEST version | ||
// when a promise has ended | ||
timer.startTimer = startTimer; | ||
}); | ||
|
||
// clear timer on unmount | ||
useEffect( | ||
() => () => { | ||
log.debug('Removing timer on unmount'); | ||
clearTimer(); | ||
}, | ||
[clearTimer], | ||
); | ||
|
||
return [startTimer, clearTimer, isRunning]; | ||
}; | ||
|
||
export default useTiming; |