Skip to content

Commit

Permalink
Add: Implement a useTiming hook
Browse files Browse the repository at this point in the history
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
bjoernricks committed Jun 7, 2024
1 parent 3371ec5 commit 46a9a7f
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
57 changes: 57 additions & 0 deletions src/web/hooks/__tests__/useTiming.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* 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, wait} from 'web/utils/testing';

import useTiming from '../useTiming';

const TestComponent = () => {
const [value, setValue] = useState(0);
const updateValue = () => setValue(1);
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');

act(() => {
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');
});
});
109 changes: 109 additions & 0 deletions src/web/hooks/useTiming.js
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;

0 comments on commit 46a9a7f

Please sign in to comment.