Skip to content

Commit

Permalink
Adding useSetInterval and useSetTimeout within react-hooks package (m…
Browse files Browse the repository at this point in the history
  • Loading branch information
czearing committed Jul 11, 2020
1 parent c7b7480 commit e105904
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "Adding useSetInterval and useSetTimeout within react-hooks package.",
"packageName": "@uifabric/react-hooks",
"email": "czearing@outlook.com",
"dependentChangeType": "patch",
"date": "2020-07-09T20:56:34.400Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "patch",
"comment": "Fix for the test-utility safeMount to check if wrapper should unmount.",
"packageName": "@uifabric/test-utilities",
"email": "czearing@outlook.com",
"dependentChangeType": "patch",
"date": "2020-07-09T21:01:39.689Z"
}
44 changes: 44 additions & 0 deletions packages/react-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,47 @@ const Example = React.forwardRef(function Example(props:{}, forwardedRef: React.
return <div>Example</div>;
})
```

## useSetInterval

`useSetInterval: () => { setInterval, clearInterval }`

Hook which returns safe `setInterval` and `clearInterval` methods. Intervals set up using this hook will be automatically cleared when the component is unmounted.

### Example

```jsx
import { useSetInterval } from '@uifabric/react-hooks';

const MyComponent =() => {
const { setInterval, clearInterval } = useSetInterval();

// Set an interval
const id = setInterval(() => console.log('test'), 500);

// If needed, clear an interval manually.
clearInterval(id);
};
```

## useSetTimeout

`const useSetTimeout: () => { setTimeout, clearTimeout }`

Hook which returns safe `setTimeout` and `clearTimeout` methods. Timeout callbacks set up using this hook will be automatically cleared when the component is unmounted.

### Example

```jsx
import { useSetInterval } from '@uifabric/react-hooks';

const MyComponent = () => {
const { setTimeout, clearTimeout } = useSetTimeout();

// Set a timeout
const id = setTimeout(() => console.log('test'), 500);

// If needed, clear an timeout manually.
clearTimeout(id);
};
```
132 changes: 75 additions & 57 deletions packages/react-hooks/etc/react-hooks.api.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,75 @@
## API Report File for "@uifabric/react-hooks"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import { Async } from '@uifabric/utilities';
import * as React from 'react';
import { Ref } from 'react';

// @public (undocumented)
export type ChangeCallback<TElement extends HTMLElement, TValue> = (ev: React.FormEvent<TElement> | undefined, newValue: TValue | undefined) => void;

// @public
export interface IUseBooleanCallbacks {
setFalse: () => void;
setTrue: () => void;
toggle: () => void;
}

// @public
export function useAsync(): Async;

// @public
export function useBoolean(initialState: boolean): [boolean, IUseBooleanCallbacks];

// @public
export function useConst<T>(initialValue: T | (() => T)): T;

// @public
export function useConstCallback<T extends (...args: any[]) => any>(callback: T): T;

// @public
export function useControllableValue<TValue, TElement extends HTMLElement>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined): Readonly<[TValue | undefined, (newValue: TValue | undefined) => void]>;

// @public (undocumented)
export function useControllableValue<TValue, TElement extends HTMLElement, TCallback extends ChangeCallback<TElement, TValue> | undefined>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev?: React.FormEvent<TElement>) => void]>;

// @public
export function useForceUpdate(): () => void;

// @public
export function useId(prefix?: string, providedId?: string): string;

// @public
export function useMergedRefs<T>(...refs: Ref<T>[]): (instance: T) => void;

// @public
export function useOnEvent<TElement extends Element, TEvent extends Event>(element: React.RefObject<TElement | undefined | null> | TElement | Window | undefined | null, eventName: string, callback: (ev: TEvent) => void, useCapture?: boolean): void;

// @public
export function usePrevious<T>(value: T): T | undefined;


// (No @packageDocumentation comment for this package)

```
## API Report File for "@uifabric/react-hooks"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import { Async } from '@uifabric/utilities';
import * as React from 'react';
import { Ref } from 'react';

// @public (undocumented)
export type ChangeCallback<TElement extends HTMLElement, TValue> = (ev: React.FormEvent<TElement> | undefined, newValue: TValue | undefined) => void;

// @public
export interface IUseBooleanCallbacks {
setFalse: () => void;
setTrue: () => void;
toggle: () => void;
}

// @public
export function useAsync(): Async;

// @public
export function useBoolean(initialState: boolean): [boolean, IUseBooleanCallbacks];

// @public
export function useConst<T>(initialValue: T | (() => T)): T;

// @public
export function useConstCallback<T extends (...args: any[]) => any>(callback: T): T;

// @public
export function useControllableValue<TValue, TElement extends HTMLElement>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined): Readonly<[TValue | undefined, (newValue: TValue | undefined) => void]>;

// @public (undocumented)
export function useControllableValue<TValue, TElement extends HTMLElement, TCallback extends ChangeCallback<TElement, TValue> | undefined>(controlledValue: TValue | undefined, defaultUncontrolledValue: TValue | undefined, onChange: TCallback): Readonly<[TValue | undefined, (newValue: TValue | undefined, ev?: React.FormEvent<TElement>) => void]>;

// @public
export function useForceUpdate(): () => void;

// @public
export function useId(prefix?: string, providedId?: string): string;

// @public
export function useMergedRefs<T>(...refs: Ref<T>[]): (instance: T) => void;

// @public
export function useOnEvent<TElement extends Element, TEvent extends Event>(element: React.RefObject<TElement | undefined | null> | TElement | Window | undefined | null, eventName: string, callback: (ev: TEvent) => void, useCapture?: boolean): void;

// @public
export function usePrevious<T>(value: T): T | undefined;

// @public
export const useSetInterval: () => UseSetIntervalReturnType;

// @public (undocumented)
export type UseSetIntervalReturnType = {
setInterval: (callback: () => void, duration: number) => number;
clearInterval: (id: number) => void;
};

// @public
export const useSetTimeout: () => UseSetTimeoutReturnType;

// @public (undocumented)
export type UseSetTimeoutReturnType = {
setTimeout: (callback: () => void, duration: number) => number;
clearTimeout: (id: number) => void;
};


// (No @packageDocumentation comment for this package)

```
1 change: 1 addition & 0 deletions packages/react-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@types/webpack-env": "1.15.1",
"@uifabric/build": "^7.0.0",
"@uifabric/tslint-rules": "^7.2.2",
"@uifabric/test-utilities": "^7.0.31",
"enzyme": "~3.10.0",
"enzyme-adapter-react-16": "^1.15.0",
"react": "16.8.6",
Expand Down
2 changes: 2 additions & 0 deletions packages/react-hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from './useAsync';
export * from './useOnEvent';
export * from './useForceUpdate';
export * from './usePrevious';
export * from './useSetInterval';
export * from './useSetTimeout';
98 changes: 98 additions & 0 deletions packages/react-hooks/src/useSetInterval.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as React from 'react';
import { useSetInterval, UseSetIntervalReturnType } from './useSetInterval';
import { safeMount } from '@uifabric/test-utilities';

describe('useSetInterval', () => {
// Initialization
let timesCalled = 0;

jest.useFakeTimers();
const TestComponent = React.forwardRef((props: unknown, ref: React.Ref<{ clearInterval: () => void }>) => {
const { setInterval, clearInterval } = useSetInterval();
const { current: state } = React.useRef<{ id: number }>({ id: 0 });

React.useImperativeHandle(
ref,
() => ({
clearInterval: () => clearInterval(state.id),
}),
[clearInterval],
);

state.id = setInterval(() => {
timesCalled++;
}, 0);

return <div />;
});

// Cleanup
afterEach(() => {
timesCalled = 0;
});

it('updates value when mounted', () => {
safeMount(<TestComponent />, () => {
expect(timesCalled).toEqual(0);

jest.runOnlyPendingTimers();

expect(timesCalled).toEqual(1);

jest.runOnlyPendingTimers();

expect(timesCalled).toEqual(2);
});
});

it('does not regenerate methods on multiple calls', () => {
let lastResult: UseSetIntervalReturnType | undefined;

const Test = () => {
lastResult = useSetInterval();
return null;
};

safeMount(<Test />, wrapper => {
const result1 = lastResult!;
lastResult = undefined;

// Cause a re-render.
wrapper.setProps({});

const result2 = lastResult!;

expect(result1.setInterval).toBeTruthy();
expect(result1.clearInterval).toBeTruthy();
expect(result1.setInterval).toBe(result2.setInterval);
expect(result1.clearInterval).toBe(result2.clearInterval);
});
});

it('does not execute the interval when unmounted', () => {
safeMount(<TestComponent />, wrapper => {
expect(timesCalled).toEqual(0);

wrapper.unmount();

jest.runOnlyPendingTimers();

expect(timesCalled).toEqual(0);
});
});

it('can cancel intervals', () => {
const ref = React.createRef<{ clearInterval: () => void }>();
safeMount(<TestComponent ref={ref} />, () => {
jest.runOnlyPendingTimers();

expect(timesCalled).toEqual(1);

ref.current!.clearInterval();

jest.runOnlyPendingTimers();

expect(timesCalled).toEqual(1);
});
});
});
40 changes: 40 additions & 0 deletions packages/react-hooks/src/useSetInterval.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import { useConst } from './useConst';
import { useEffect } from 'react';

export type UseSetIntervalReturnType = {
setInterval: (callback: () => void, duration: number) => number;
clearInterval: (id: number) => void;
};

/**
* Returns a wrapper function for `setInterval` which automatically handles disposal.
*/
export const useSetInterval = (): UseSetIntervalReturnType => {
const intervalIds = useConst<Record<number, number>>({});

useEffect(
() => () => {
for (const id of Object.keys(intervalIds)) {
// tslint:disable-next-line:no-any
clearInterval(id as any);
}
},
[],
);

return {
setInterval: React.useCallback((func: () => void, duration: number): number => {
const id = (setInterval(func, duration) as unknown) as number;

intervalIds[id] = 1;

return id;
}, []),

clearInterval: React.useCallback((id: number): void => {
delete intervalIds[id];
clearInterval(id);
}, []),
};
};
Loading

0 comments on commit e105904

Please sign in to comment.