Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core/presentation): add useInterval hook (#8609)
* feat(core/presentation): add useInterval hook * feat(core/presentation): add spec for useInterval hook
- Loading branch information
Erik Munson
committed
Oct 1, 2020
1 parent
619ad9c
commit 0463ecb
Showing
3 changed files
with
125 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
96 changes: 96 additions & 0 deletions
96
app/scripts/modules/core/src/presentation/hooks/useInterval.hook.spec.tsx
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,96 @@ | ||
import React from 'react'; | ||
import { mount } from 'enzyme'; | ||
import { useInterval } from './useInterval.hook'; | ||
|
||
describe('useInterval hook', () => { | ||
beforeEach(() => jasmine.clock().install()); | ||
afterEach(() => jasmine.clock().uninstall()); | ||
|
||
function Component(props: any) { | ||
const { callback, interval } = props; | ||
|
||
useInterval(callback, interval); | ||
|
||
return <></>; | ||
} | ||
|
||
it('calls the callback on the specified interval', () => { | ||
const spy = jasmine.createSpy('callback'); | ||
const component = mount(<Component callback={spy} interval={1000} />); | ||
|
||
expect(spy).toHaveBeenCalledTimes(0); | ||
|
||
// Tick forward to slightly before the polling interval should kick in | ||
jasmine.clock().tick(900); | ||
component.setProps({}); | ||
|
||
expect(spy).toHaveBeenCalledTimes(0); | ||
|
||
// Tick forward to past the first interval | ||
jasmine.clock().tick(200); | ||
component.setProps({}); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
|
||
// Second interval | ||
jasmine.clock().tick(1000); | ||
component.setProps({}); | ||
expect(spy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('resets the interval when changed', () => { | ||
const spy = jasmine.createSpy('callback'); | ||
const component = mount(<Component callback={spy} interval={1000} />); | ||
|
||
expect(spy).toHaveBeenCalledTimes(0); | ||
|
||
// Tick forward to slightly before the polling interval should kick in | ||
jasmine.clock().tick(900); | ||
component.setProps({}); | ||
|
||
expect(spy).toHaveBeenCalledTimes(0); | ||
|
||
// Change / reset interval | ||
component.setProps({ interval: 5000 }); | ||
|
||
// Tick forward to slightly past the original interval (clock is now at 1200) | ||
jasmine.clock().tick(200); | ||
component.setProps({}); | ||
expect(spy).toHaveBeenCalledTimes(0); | ||
|
||
// Tick forward to first iteration of new interval | ||
jasmine.clock().tick(5000); | ||
component.setProps({}); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('does not call a callback when none is provided', () => { | ||
const spy = jasmine.createSpy('callback'); | ||
const component = mount(<Component callback={spy} interval={1000} />); | ||
|
||
expect(spy).toHaveBeenCalledTimes(0); | ||
|
||
jasmine.clock().tick(1200); | ||
component.setProps({}); | ||
|
||
expect(spy).toHaveBeenCalledTimes(1); | ||
|
||
component.setProps({ callback: null }); | ||
|
||
// Because we got rid of the callback, the hook should stop the interval and | ||
// throw away the previous callback | ||
jasmine.clock().tick(1200); | ||
component.setProps({}); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
|
||
const newSpy = jasmine.createSpy('newCallback'); | ||
|
||
component.setProps({ callback: newSpy }); | ||
|
||
// Passing a new callback after null should restart the interval | ||
expect(newSpy).toHaveBeenCalledTimes(0); | ||
|
||
jasmine.clock().tick(1200); | ||
component.setProps({}); | ||
expect(newSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
28 changes: 28 additions & 0 deletions
28
app/scripts/modules/core/src/presentation/hooks/useInterval.hook.ts
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,28 @@ | ||
import { useEffect } from 'react'; | ||
|
||
import { useLatestCallback } from './useLatestCallback.hook'; | ||
|
||
/** | ||
* A react hook which invokes a callback function on a specific millisecond interval. | ||
* Works just like setInterval(), but in a hook. | ||
* | ||
* If no callback is provided during a render (i.e. passing null or another falsey value) | ||
* then no interval will be set and the existing interval will be canceled if one is already running. | ||
* This is useful for components which only need to run an interval conditionally but not all the time. | ||
* | ||
* Passing a different/changed interval will reset the cycle with the new interval time. | ||
* * | ||
* @param callback the callback to be invoked at every interval, or null/undefined | ||
* for no interval at all | ||
* @param interval the length of time in milliseconds between each interval | ||
* @returns void | ||
*/ | ||
export function useInterval(callback: () => any, interval: number): void { | ||
const stableCallback = useLatestCallback(callback); | ||
|
||
useEffect(() => { | ||
const id = !!callback && setInterval(stableCallback, interval); | ||
|
||
return () => id && clearInterval(id); | ||
}, [!!callback, interval]); | ||
} |