Skip to content

Commit

Permalink
feat(core/presentation): add useInterval hook (#8609)
Browse files Browse the repository at this point in the history
* 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
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/presentation/hooks/index.ts
Expand Up @@ -6,6 +6,7 @@ export * from './useDebouncedValue.hook';
export * from './useDeepObjectDiff.hook';
export * from './useEventListener.hook';
export * from './useForceUpdate.hook';
export * from './useInterval.hook';
export * from './useIsMobile.hook';
export * from './useIsMountedRef.hook';
export * from './useLatestCallback.hook';
Expand Down
@@ -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);
});
});
@@ -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]);
}

0 comments on commit 0463ecb

Please sign in to comment.