Merge https://github.com/streamich/react-use into feat/enhance_useDebounce

This commit is contained in:
xobotyi 2019-08-26 00:41:30 +03:00
commit 036edfdeee
66 changed files with 2044 additions and 234 deletions

View File

@ -9,10 +9,19 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120
[*.{ts, tsx}]
ij_typescript_enforce_trailing_comma = keep
ij_typescript_use_double_quotes = false
ij_typescript_force_quote_style = true
ij_typescript_align_imports = false
ij_typescript_align_multiline_ternary_operation = false
ij_typescript_align_multiline_parameters_in_calls = false
ij_typescript_align_multiline_parameters = false
ij_typescript_align_multiline_chained_methods = false
ij_typescript_else_on_new_line = false
ij_typescript_catch_on_new_line = false
ij_typescript_spaces_within_interpolation_expressions = false
[*.md]
max_line_length = 0

26
.github/workflows/pull_request_ci.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Node CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
yarn install --pure-lockfile
yarn build
yarn test
env:
CI: true

View File

@ -1,3 +1,96 @@
## [11.3.1](https://github.com/streamich/react-use/compare/v11.3.0...v11.3.1) (2019-08-25)
### Bug Fixes
* update createReducer to fix initial state ([fd083f2](https://github.com/streamich/react-use/commit/fd083f2))
# [11.3.0](https://github.com/streamich/react-use/compare/v11.2.0...v11.3.0) (2019-08-25)
### Features
* add usePreviousDistinct ([#551](https://github.com/streamich/react-use/issues/551)) ([6c3e569](https://github.com/streamich/react-use/commit/6c3e569))
# [11.2.0](https://github.com/streamich/react-use/compare/v11.1.1...v11.2.0) (2019-08-25)
### Features
* add useCircularIterate ([8d84340](https://github.com/streamich/react-use/commit/8d84340))
## [11.1.1](https://github.com/streamich/react-use/compare/v11.1.0...v11.1.1) (2019-08-25)
### Bug Fixes
* [#550](https://github.com/streamich/react-use/issues/550) ([2617d74](https://github.com/streamich/react-use/commit/2617d74))
# [11.1.0](https://github.com/streamich/react-use/compare/v11.0.2...v11.1.0) (2019-08-25)
### Features
* 🎸 add useHarmonicIntervalFn() hook ([d9f21e3](https://github.com/streamich/react-use/commit/d9f21e3))
## [11.0.2](https://github.com/streamich/react-use/compare/v11.0.1...v11.0.2) (2019-08-23)
### Bug Fixes
* **useSetState:** memoize setState callback ([0275329](https://github.com/streamich/react-use/commit/0275329))
* **useSetState:** memoize setState callback ([16f023f](https://github.com/streamich/react-use/commit/16f023f))
## [11.0.1](https://github.com/streamich/react-use/compare/v11.0.0...v11.0.1) (2019-08-23)
### Bug Fixes
* correct useSpring() hook behaviour ([d7a622d](https://github.com/streamich/react-use/commit/d7a622d))
# [11.0.0](https://github.com/streamich/react-use/compare/v10.8.0...v11.0.0) (2019-08-22)
### Features
* add cancel and reset methods to useTimeout ([283045a](https://github.com/streamich/react-use/commit/283045a))
* add useTimeoutFn ([284e6fd](https://github.com/streamich/react-use/commit/284e6fd))
### BREAKING CHANGES
* useTimeout now returns a tuple
# [10.8.0](https://github.com/streamich/react-use/compare/v10.7.1...v10.8.0) (2019-08-20)
### Bug Fixes
* Reworked useBattery hook ([1069060](https://github.com/streamich/react-use/commit/1069060))
* succeed useRafLoop tests ([09167df](https://github.com/streamich/react-use/commit/09167df))
### Features
* 🎸 support useBattery hook on server side ([5d31cf0](https://github.com/streamich/react-use/commit/5d31cf0))
* 🎸 use only one useState and one useEffect call ([2d0fabf](https://github.com/streamich/react-use/commit/2d0fabf))
## [10.7.1](https://github.com/streamich/react-use/compare/v10.7.0...v10.7.1) (2019-08-20)
### Bug Fixes
* async test warnings ([#543](https://github.com/streamich/react-use/issues/543)) ([7af237e](https://github.com/streamich/react-use/commit/7af237e))
# [10.7.0](https://github.com/streamich/react-use/compare/v10.6.4...v10.7.0) (2019-08-19)
### Features
* 🎸 add useUpsert ([6875e13](https://github.com/streamich/react-use/commit/6875e13))
* 🎸 export useUpsert from index ([3eda2b2](https://github.com/streamich/react-use/commit/3eda2b2))
* add useUpsert ([a7c2899](https://github.com/streamich/react-use/commit/a7c2899))
## [10.6.4](https://github.com/streamich/react-use/compare/v10.6.3...v10.6.4) (2019-08-19)

View File

@ -79,9 +79,10 @@
<br/>
- [**Animations**](./docs/Animations.md)
- [`useRaf`](./docs/useRaf.md) &mdash; re-renders component on each `requestAnimationFrame`.
- [`useInterval`](./docs/useInterval.md) &mdash; re-renders component on a set interval using `setInterval`.
- [`useInterval`](./docs/useInterval.md) and [`useHarmonicIntervalFn`](./docs/useHarmonicIntervalFn.md) &mdash; re-renders component on a set interval using `setInterval`.
- [`useSpring`](./docs/useSpring.md) &mdash; interpolates number over time according to spring dynamics.
- [`useTimeout`](./docs/useTimeout.md) &mdash; returns true after a timeout.
- [`useTimeout`](./docs/useTimeout.md) &mdash; re-renders component after a timeout.
- [`useTimeoutFn`](./docs/useTimeoutFn.md) &mdash; calls given function after a timeout. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/animation-usetimeoutfn--demo)
- [`useTween`](./docs/useTween.md) &mdash; re-renders component, while tweening a number from 0 to 1. [![][img-demo]](https://codesandbox.io/s/52990wwzyl)
- [`useUpdate`](./docs/useUpdate.md) &mdash; returns a callback, which re-renders component when called.
<br/>
@ -126,6 +127,7 @@
- [`usePrevious`](./docs/usePrevious.md) &mdash; returns the previous state or props.
- [`useObservable`](./docs/useObservable.md) &mdash; tracks latest value of an `Observable`.
- [`useSetState`](./docs/useSetState.md) &mdash; creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0)
- [`useStateList`](./docs/useStateList.md) &mdash; circularly iterates over an array.
- [`useToggle` and `useBoolean`](./docs/useToggle.md) &mdash; tracks state of a boolean.
- [`useCounter` and `useNumber`](./docs/useCounter.md) &mdash; tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo)
- [`useList`](./docs/useList.md) &mdash; tracks state of an array.

View File

@ -45,7 +45,7 @@ const Demo = ({ initialCount = 1 }) => {
<p>count: {state.count}</p>
<button onClick={() => dispatch(addAndReset())}>Add and reset</button>
<button
onClick={() => dispatch({ type: 'reset', payload: initialCount })}
onClick={() => dispatch({ type: 'reset', payload: { count: initialCount }})}
>
Reset
</button>

View File

@ -2,6 +2,9 @@
React sensor hook that tracks battery status.
>**Note:** current `BatteryManager` API state is obsolete.
>Although it may still work in some browsers, its use is discouraged since it could be removed at any time.
## Usage
@ -9,12 +12,47 @@ React sensor hook that tracks battery status.
import {useBattery} from 'react-use';
const Demo = () => {
const state = useBattery();
const batteryState = useBattery();
if (!batteryState.isSupported) {
return (
<div>
<strong>Battery sensor</strong>: <span>not supported</span>
</div>
);
}
if (!batteryState.fetched) {
return (
<div>
<strong>Battery sensor</strong>: <span>supported</span> <br />
<strong>Battery state</strong>: <span>fetching</span>
</div>
);
}
return (
<pre>
{JSON.stringify(state, null, 2)}
</pre>
<div>
<strong>Battery sensor</strong>:&nbsp;&nbsp; <span>supported</span> <br />
<strong>Battery state</strong>: <span>fetched</span> <br />
<strong>Charge level</strong>:&nbsp;&nbsp; <span>{ (batteryState.level * 100).toFixed(0) }%</span> <br />
<strong>Charging</strong>:&nbsp;&nbsp; <span>{ batteryState.charging ? 'yes' : 'no' }</span> <br />
<strong>Charging time</strong>:&nbsp;&nbsp;
<span>{ batteryState.chargingTime ? batteryState.chargingTime : 'finished' }</span> <br />
<strong>Discharging time</strong>:&nbsp;&nbsp; <span>{ batteryState.dischargingTime }</span>
</div>
);
};
```
## Reference
```ts
const {isSupported, level, charging, dischargingTime, chargingTime} = useBattery();
```
- **`isSupported`**_`: boolean`_ - whether browser/devise supports BatteryManager;
- **`fetched`**_`: boolean`_ - whether battery state is fetched;
- **`level`**_`: number`_ - representing the system's battery charge level scaled to a value between 0.0 and 1.0.
- **`charging`**_`: boolean`_ - indicating whether or not the battery is currently being charged.
- **`dischargingTime`**_`: number`_ - remaining time in seconds until the battery is completely discharged and the system will suspend.
- **`chargingTime`**_`: number`_ - remaining time in seconds until the battery is fully charged, or 0 if the battery is already fully charged.

View File

@ -0,0 +1,14 @@
# `useHarmonicIntervalFn`
Same as [`useInterval`](./useInterval.md) hook, but triggers all effects with the same delay
at the same time.
For example, this allows you to create ticking clocks on the page which re-render second counter
all at the same time.
## Reference
```js
useHarmonicIntervalFn(fn, delay?: number)
```

View File

@ -2,7 +2,12 @@
React side-effect hook that locks scrolling on the body element. Useful for modal and other overlay components.
## Usage
Accepts ref object pointing to any HTML element as second parameter. Parent body element will be found and it's scroll will be locked/unlocked. It is needed to proper iFrame handling.
By default it uses body element of script's parent window.
>Note: To improve performance you can pass body's or iframe's ref object, thus no parent lookup will be performed
## Usage
```jsx
import {useLockBodyScroll, useToggle} from 'react-use';
@ -25,7 +30,8 @@ const Demo = () => {
## Reference
```ts
useLockBodyScroll(enabled?: boolean = true);
useLockBodyScroll(locked: boolean = true, elementRef?: RefObject<HTMLElement>);
```
- `enabled` &mdash; Hook will lock scrolling on the body element if `true`, defaults to `true`
- `locked` &mdash; Hook will lock scrolling on the body element if `true`, defaults to `true`
- `elementRef` &mdash; The element ref object to find the body element. Can be either a ref to body or iframe element.

View File

@ -0,0 +1,51 @@
# `usePreviousDistinct`
Just like `usePrevious` but it will only update once the value actually changes. This is important when other
hooks are involved and you aren't just interested in the previous props version, but want to know the previous
distinct value
## Usage
```jsx
import {usePreviousDistinct, useCounter} from 'react-use';
const Demo = () => {
const [count, { inc: relatedInc }] = useCounter(0);
const [unrelatedCount, { inc }] = useCounter(0);
const prevCount = usePreviousDistinct(count);
return (
<p>
Now: {count}, before: {prevCount}
<button onClick={() => relatedInc()}>Increment</button>
Unrelated: {unrelatedCount}
<button onClick={() => inc()}>Increment Unrelated</button>
</p>
);
};
```
You can also provide a way of identifying the value as unique. By default, a strict equals is used.
```jsx
import {usePreviousDistinct} from 'react-use';
const Demo = () => {
const [str, setStr] = React.useState("something_lowercase");
const [unrelatedCount] = React.useState(0);
const prevStr = usePreviousDistinct(str, (prev, next) => (prev && prev.toUpperCase()) === next.toUpperCase());
return (
<p>
Now: {count}, before: {prevCount}
Unrelated: {unrelatedCount}
</p>
);
};
```
## Reference
```ts
const prevState = usePreviousDistinct = <T>(state: T, compare?: (prev: T | undefined, next: T) => boolean): T;
```

View File

@ -1,5 +1,8 @@
# `useRefMounted`
>**DEPRECATED**
>This method is obsolete, use `useMountedState` instead.
Lifecycle hook that tracks if component is mounted. Returns a ref, which has a
boolean `.current` property.

25
docs/useStateList.md Normal file
View File

@ -0,0 +1,25 @@
# `useStateList`
React state hook that circularly iterates over an array.
## Usage
```jsx
import { useStateList } from 'react-use';
const stateSet = ['first', 'second', 'third', 'fourth', 'fifth'];
const Demo = () => {
const {state, prev, next} = useStateList(stateSet);
return (
<div>
<pre>{state}</pre>
<button onClick={() => prev()}>prev</button>
<button onClick={() => next()}>next</button>
</div>
);
};
```
> If the `stateSet` is changed by a shorter one the hook will select the last element of it.

View File

@ -1,15 +1,48 @@
# `useTimeout`
Returns `true` after a specified number of milliseconds.
Re-renders the component after a specified number of milliseconds.
Provides handles to cancel and/or reset the timeout.
## Usage
```jsx
import { useTimeout } from 'react-use';
const Demo = () => {
const ready = useTimeout(2000);
function TestComponent(props: { ms?: number } = {}) {
const ms = props.ms || 5000;
const [isReady, cancel] = useTimeout(ms);
return <div>Ready: {ready ? 'Yes' : 'No'}</div>;
return (
<div>
{ isReady() ? 'I\'m reloaded after timeout' : `I will be reloaded after ${ ms / 1000 }s` }
{ isReady() === false ? <button onClick={ cancel }>Cancel</button> : '' }
</div>
);
}
const Demo = () => {
return (
<div>
<TestComponent />
<TestComponent ms={ 10000 } />
</div>
);
};
```
## Reference
```ts
const [
isReady: () => boolean | null,
cancel: () => void,
reset: () => void,
] = useTimeout(ms: number = 0);
```
- **`isReady`**_` :()=>boolean|null`_ - function returning current timeout state:
- `false` - pending re-render
- `true` - re-render performed
- `null` - re-render cancelled
- **`cancel`**_` :()=>void`_ - cancel the timeout (component will not be re-rendered)
- **`reset`**_` :()=>void`_ - reset the timeout

65
docs/useTimeoutFn.md Normal file
View File

@ -0,0 +1,65 @@
# `useTimeoutFn`
Calls given function after specified amount of milliseconds.
**Note:** this hook does not re-render component by itself.
Automatically cancels timeout on component unmount.
Automatically resets timeout on delay change.
## Usage
```jsx
import * as React from 'react';
import { useTimeoutFn } from 'react-use';
const Demo = () => {
const [state, setState] = React.useState('Not called yet');
function fn() {
setState(`called at ${Date.now()}`);
}
const [isReady, cancel, reset] = useTimeoutFn(fn, 5000);
const cancelButtonClick = useCallback(() => {
if (isReady() === false) {
cancel();
setState(`cancelled`);
} else {
reset();
setState('Not called yet');
}
}, []);
const readyState = isReady();
return (
<div>
<div>{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}</div>
<button onClick={cancelButtonClick}> {readyState === false ? 'cancel' : 'restart'} timeout</button>
<br />
<div>Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}</div>
<div>{state}</div>
</div>
);
};
```
## Reference
```ts
const [
isReady: () => boolean | null,
cancel: () => void,
reset: () => void,
] = useTimeoutFn(fn: Function, ms: number = 0);
```
- **`fn`**_`: Function`_ - function that will be called;
- **`ms`**_`: number`_ - delay in milliseconds;
- **`isReady`**_`: ()=>boolean|null`_ - function returning current timeout state:
- `false` - pending
- `true` - called
- `null` - cancelled
- **`cancel`**_`: ()=>void`_ - cancel the timeout
- **`reset`**_`: ()=>void`_ - reset the timeout

28
docs/useUpsert.md Normal file
View File

@ -0,0 +1,28 @@
# `useUpsert`
Superset of `useList`. Provides an additional method to upsert (update or insert) an element into the list.
## Usage
```jsx
import {useUpsert} from 'react-use';
const Demo = () => {
const comparisonFunction = (a: DemoType, b: DemoType) => {
return a.id === b.id;
};
const [list, { set, upsert, remove }] = useUpsert(comparisonFunction, initialItems);
return (
<div style={{ display: 'inline-flex', flexDirection: 'column' }}>
{list.map((item: DemoType, index: number) => (
<div key={item.id}>
<input value={item.text} onChange={e => upsert({ ...item, text: e.target.value })} />
<button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => upsert({ id: (list.length + 1).toString(), text: '' })}>Add item</button>
<button onClick={() => set([])}>Reset</button>
</div>
);
};
```

View File

@ -1,6 +1,6 @@
{
"name": "react-use",
"version": "10.6.4",
"version": "11.3.1",
"description": "Collection of React Hooks",
"main": "lib/index.js",
"module": "esm/index.js",
@ -51,6 +51,7 @@
"react-fast-compare": "^2.0.4",
"react-wait": "^0.3.0",
"screenfull": "^4.1.0",
"set-harmonic-interval": "^1.0.0",
"throttle-debounce": "^2.0.1",
"ts-easing": "^0.2.0"
},
@ -73,7 +74,7 @@
"@storybook/addon-options": "5.1.11",
"@storybook/react": "5.1.11",
"@testing-library/react-hooks": "2.0.1",
"@types/jest": "24.0.17",
"@types/jest": "24.0.18",
"@types/react": "16.9.2",
"babel-core": "6.26.3",
"babel-loader": "8.0.6",
@ -83,11 +84,13 @@
"husky": "3.0.4",
"jest": "24.9.0",
"keyboardjs": "2.5.1",
"lint-staged": "9.2.3",
"lint-staged": "9.2.4",
"markdown-loader": "5.1.0",
"prettier": "1.18.2",
"raf-stub": "3.0.0",
"react": "16.9.0",
"react-dom": "16.9.0",
"react-frame-component": "4.1.1",
"react-spring": "8.0.27",
"react-test-renderer": "16.9.0",
"rebound": "0.1.0",
@ -95,10 +98,10 @@
"redux-thunk": "2.3.0",
"rimraf": "3.0.0",
"rxjs": "6.5.2",
"semantic-release": "15.13.21",
"semantic-release": "15.13.24",
"ts-loader": "6.0.4",
"ts-node": "8.3.0",
"tslint": "5.18.0",
"tslint": "5.19.0",
"tslint-config-prettier": "1.18.0",
"tslint-eslint-rules": "5.4.0",
"tslint-plugin-prettier": "2.0.1",

View File

@ -4,9 +4,36 @@ import { useBattery } from '..';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
const state = useBattery();
const batteryState = useBattery();
return <pre>{JSON.stringify(state, null, 2)}</pre>;
if (!batteryState.isSupported) {
return (
<div>
<strong>Battery sensor</strong>: <span>not supported</span>
</div>
);
}
if (!batteryState.fetched) {
return (
<div>
<strong>Battery sensor</strong>: <span>supported</span> <br />
<strong>Battery state</strong>: <span>fetching</span>
</div>
);
}
return (
<div>
<strong>Battery sensor</strong>:&nbsp;&nbsp; <span>supported</span> <br />
<strong>Battery state</strong>: <span>fetched</span> <br />
<strong>Charge level</strong>:&nbsp;&nbsp; <span>{(batteryState.level * 100).toFixed(0)}%</span> <br />
<strong>Charging</strong>:&nbsp;&nbsp; <span>{batteryState.charging ? 'yes' : 'no'}</span> <br />
<strong>Charging time</strong>:&nbsp;&nbsp;
<span>{batteryState.chargingTime ? batteryState.chargingTime : 'finished'}</span> <br />
<strong>Discharging time</strong>:&nbsp;&nbsp; <span>{batteryState.dischargingTime}</span>
</div>
);
};
storiesOf('Sensors|useBattery', module)

View File

@ -1,6 +1,6 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useGetSetState, useSetState } from '..';
import { useGetSetState } from '..';
import ShowDocs from './util/ShowDocs';
const Demo = () => {

View File

@ -0,0 +1,69 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useHarmonicIntervalFn, useInterval, useTimeoutFn } from '..';
import ShowDocs from './util/ShowDocs';
const Clock: React.FC<{ useInt: typeof useHarmonicIntervalFn }> = ({ useInt }) => {
const [count, setCount] = React.useState(0);
useInt(() => {
setCount(cnt => cnt + 1);
}, 1000);
let m: number | string = Math.floor(count / 60);
let s: number | string = count % 60;
m = m < 10 ? '0' + m : String(m);
s = s < 10 ? '0' + s : String(s);
const style: React.CSSProperties = {
padding: '20px 5px',
border: '1px solid #fafafa',
float: 'left',
fontFamily: 'monospace',
};
return <div style={style}>{m + ':' + s}</div>;
};
const Demo: React.FC<{ useInt: typeof useHarmonicIntervalFn }> = ({ useInt }) => {
const [showSecondClock, setShowSecondClock] = React.useState(false);
useTimeoutFn(() => {
setShowSecondClock(true);
}, 500);
const headingStyle: React.CSSProperties = {
fontFamily: 'sans',
fontSize: '20px',
padding: '0',
lineHeight: '1.5em',
};
const rowStyle: React.CSSProperties = {
width: '100%',
clear: 'both',
};
return (
<>
<div style={rowStyle}>
<h2 style={headingStyle}>{useInt.name}</h2>
<Clock useInt={useInt} />
{showSecondClock ? <Clock useInt={useInt} /> : null}
</div>
</>
);
};
storiesOf('Animation|useHarmonicIntervalFn', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useHarmonicIntervalFn.md')} />)
.add('Demo', () => (
<>
<Demo useInt={useInterval} />
<br />
<br />
<br />
<br />
<br />
<Demo useInt={useHarmonicIntervalFn} />
</>
));

View File

@ -28,6 +28,6 @@ const Demo = () => {
);
};
storiesOf('Side effects|useInterval', module)
storiesOf('Animation|useInterval', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useInterval.md')} />)
.add('Demo', () => <Demo />);

View File

@ -1,5 +1,7 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useRef } from 'react';
import Frame from 'react-frame-component';
import { useLockBodyScroll, useToggle } from '..';
import ShowDocs from './util/ShowDocs';
@ -27,6 +29,30 @@ const AnotherComponent = () => {
);
};
const IframeComponent = () => {
const [mainLocked, toggleMainLocked] = useToggle(false);
const [iframeLocked, toggleIframeLocked] = useToggle(false);
const iframeElementRef = useRef<HTMLIFrameElement>(null);
useLockBodyScroll(mainLocked);
useLockBodyScroll(iframeLocked, iframeElementRef);
return (
<div style={{ height: '200vh' }}>
<Frame style={{ height: '50vh', width: '50vw' }}>
<div style={{ height: '200vh' }} ref={iframeElementRef}>
<button onClick={() => toggleMainLocked()} style={{ position: 'fixed', left: 0, top: 0 }}>
{mainLocked ? 'Unlock' : 'Lock'} main window scroll
</button>
<button onClick={() => toggleIframeLocked()} style={{ position: 'fixed', left: 0, top: 64 }}>
{iframeLocked ? 'Unlock' : 'Lock'} iframe window scroll
</button>
</div>
</Frame>
</div>
);
};
storiesOf('Side effects|useLockBodyScroll', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useLockBodyScroll.md')} />)
.add('Demo', () => <Demo />)
@ -34,5 +60,7 @@ storiesOf('Side effects|useLockBodyScroll', module)
<>
<AnotherComponent />
<Demo />
<IframeComponent />
</>
));
))
.add('Iframe', () => <IframeComponent />);

View File

@ -1,8 +1,7 @@
import { boolean, text, withKnobs } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useCounter } from '..';
import { useLogger } from '..';
import { useCounter, useLogger } from '..';
import ShowDocs from './util/ShowDocs';
const Demo = props => {

View File

@ -0,0 +1,23 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { usePreviousDistinct, useCounter } from '..';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
const [count, { inc: relatedInc }] = useCounter(0);
const [unrelatedCount, { inc }] = useCounter(0);
const prevCount = usePreviousDistinct(count);
return (
<p>
Now: {count}, before: {prevCount}
<button onClick={() => relatedInc()}>Increment</button>
Unrelated: {unrelatedCount}
<button onClick={() => inc()}>Increment Unrelated</button>
</p>
);
};
storiesOf('State|usePreviousDistinct', module)
.add('Docs', () => <ShowDocs md={require('../../docs/usePreviousDistinct.md')} />)
.add('Demo', () => <Demo />);

View File

@ -1,7 +1,7 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import ShowDocs from './util/ShowDocs';
import { useRafLoop } from '..';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
const [ticks, setTicks] = React.useState(0);

View File

@ -5,8 +5,15 @@ import ShowDocs from './util/ShowDocs';
const Demo = () => {
const refMounted = useRefMounted();
useRaf();
return <div>is mounted: {refMounted.current ? '👍' : '👎'}</div>;
return (
<div>
<h3>**DEPRECATED**</h3>
<h4>This method is obsolete, use `useMountedState` instead.</h4>
<span>is mounted: {refMounted.current ? '👍' : '👎'}</span>
</div>
);
};
storiesOf('Lifecycle|useRefMounted', module)

View File

@ -1,6 +1,6 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useSpring } from '..';
import useSpring from '../useSpring';
import ShowDocs from './util/ShowDocs';
const Demo = () => {

View File

@ -0,0 +1,22 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useStateList } from '..';
import ShowDocs from './util/ShowDocs';
const stateSet = ['first', 'second', 'third', 'fourth', 'fifth'];
const Demo = () => {
const { state, prev, next } = useStateList(stateSet);
return (
<div>
<pre>{state}</pre>
<button onClick={() => prev()}>prev</button>
<button onClick={() => next()}>next</button>
</div>
);
};
storiesOf('State|useStateList', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useStateList.md')} />)
.add('Demo', () => <Demo />);

View File

@ -3,10 +3,25 @@ import * as React from 'react';
import { useTimeout } from '..';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
const ready = useTimeout(2e3);
function TestComponent(props: { ms?: number } = {}) {
const ms = props.ms || 5000;
const [isReady, cancel] = useTimeout(ms);
return <div>Ready: {ready ? 'Yes' : 'No'}</div>;
return (
<div>
{isReady() ? "I'm reloaded after timeout" : `I will be reloaded after ${ms / 1000}s`}
{isReady() === false ? <button onClick={cancel}>Cancel</button> : ''}
</div>
);
}
const Demo = () => {
return (
<div>
<TestComponent />
<TestComponent ms={10000} />
</div>
);
};
storiesOf('Animation|useTimeout', module)

View File

@ -0,0 +1,40 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useCallback } from 'react';
import { useTimeoutFn } from '../index';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
const [state, setState] = React.useState('Not called yet');
function fn() {
setState(`called at ${Date.now()}`);
}
const [isReady, cancel, reset] = useTimeoutFn(fn, 5000);
const cancelButtonClick = useCallback(() => {
if (isReady() === false) {
cancel();
setState(`cancelled`);
} else {
reset();
setState('Not called yet');
}
}, []);
const readyState = isReady();
return (
<div>
<div>{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}</div>
<button onClick={cancelButtonClick}> {readyState === false ? 'cancel' : 'restart'} timeout</button>
<br />
<div>Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}</div>
<div>{state}</div>
</div>
);
};
storiesOf('Animation|useTimeoutFn', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useTimeoutFn.md')} />)
.add('Demo', () => <Demo />);

View File

@ -0,0 +1,35 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useUpsert } from '../index';
import ShowDocs from './util/ShowDocs';
interface DemoType {
id: string;
text: string;
}
const initialItems: DemoType[] = [{ id: '1', text: 'Sample' }, { id: '2', text: '' }];
const Demo = () => {
const comparisonFunction = (a: DemoType, b: DemoType) => {
return a.id === b.id;
};
const [list, { set, upsert, remove }] = useUpsert(comparisonFunction, initialItems);
return (
<div style={{ display: 'inline-flex', flexDirection: 'column' }}>
{list.map((item: DemoType, index: number) => (
<div key={item.id}>
<input value={item.text} onChange={e => upsert({ ...item, text: e.target.value })} />
<button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => upsert({ id: (list.length + 1).toString(), text: '' })}>Add item</button>
<button onClick={() => set([])}>Reset</button>
</div>
);
};
storiesOf('State|useUpsert', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useUpsert.md')} />)
.add('Demo', () => <Demo />);

View File

@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import createReducer from '../createReducer';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import createReducer from '../createReducer';
it('should init reducer hook function', () => {
const useSomeReducer = createReducer();

View File

@ -107,11 +107,14 @@ describe('useAsync', () => {
return 'new value';
};
beforeEach(() => {
beforeEach(done => {
callCount = 0;
hook = renderHook(({ fn }) => useAsync(fn, [fn]), {
initialProps: { fn: initialFn },
});
hook.waitForNextUpdate().then(done);
});
it('renders the first value', () => {
@ -140,7 +143,7 @@ describe('useAsync', () => {
return `counter is ${counter} and callCount is ${callCount}`;
};
beforeEach(() => {
beforeEach(done => {
callCount = 0;
hook = renderHook(
({ fn, counter }) => {
@ -154,6 +157,8 @@ describe('useAsync', () => {
},
}
);
hook.waitForNextUpdate().then(done);
});
it('initial renders the first passed pargs', () => {

View File

@ -5,7 +5,7 @@
// does not automatically invoke the function
// and it can take arguments.
import { renderHook } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import useAsyncFn, { AsyncState } from '../useAsyncFn';
type AdderFn = (a: number, b: number) => Promise<number>;
@ -17,27 +17,26 @@ describe('useAsyncFn', () => {
describe('the callback can be awaited and return the value', () => {
let hook;
let callCount = 0;
const adder = async (a: number, b: number): Promise<number> => {
callCount++;
return a + b;
};
beforeEach(() => {
// NOTE: renderHook isn't good at inferring array types
hook = renderHook<{ fn: AdderFn }, [AsyncState<number>, AdderFn]>(({ fn }) => useAsyncFn(fn), {
initialProps: {
fn: adder,
},
initialProps: { fn: adder },
});
});
it('awaits the result', async () => {
expect.assertions(3);
const [s, callback] = hook.result.current;
const [, callback] = hook.result.current;
let result;
const result = await callback(5, 7);
await act(async () => {
result = await callback(5, 7);
});
expect(result).toEqual(12);
@ -78,13 +77,15 @@ describe('useAsyncFn', () => {
it('resolves a value derived from args', async () => {
expect.assertions(4);
const [s, callback] = hook.result.current;
const [, callback] = hook.result.current;
callback(2, 7);
act(() => {
callback(2, 7);
});
hook.rerender({ fn: adder });
await hook.waitForNextUpdate();
const [state, c] = hook.result.current;
const [state] = hook.result.current;
expect(callCount).toEqual(1);
expect(state.loading).toEqual(false);

View File

@ -100,8 +100,7 @@ it('should log an error if set with a patch different than an object', () => {
const [, set] = result.current;
expect(mockConsoleError).not.toHaveBeenCalled();
// @ts-ignore
act(() => set('not an object'));
act(() => set('not an object' as any));
expect(mockConsoleError).toHaveBeenCalledTimes(1);
expect(mockConsoleError).toHaveBeenCalledWith('useGetSetState setter patch must be an object.');

View File

@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useMap from '../useMap';
const setUp = (initialMap?: object) => renderHook(() => useMap(initialMap));
const setUp = <T extends object>(initialMap?: T) => renderHook(() => useMap(initialMap));
it('should init map and utils', () => {
const { result } = setUp({ foo: 'bar', a: 1 });
@ -28,7 +28,6 @@ it('should get corresponding value for existing provided key', () => {
let value;
act(() => {
// @ts-ignore
value = utils.get('a');
});
@ -36,12 +35,11 @@ it('should get corresponding value for existing provided key', () => {
});
it('should get undefined for non-existing provided key', () => {
const { result } = setUp({ foo: 'bar', a: 1 });
const { result } = setUp<{ foo: string; a: number; nonExisting?: any }>({ foo: 'bar', a: 1 });
const [, utils] = result.current;
let value;
act(() => {
// @ts-ignore
value = utils.get('nonExisting');
});
@ -49,11 +47,10 @@ it('should get undefined for non-existing provided key', () => {
});
it('should set new key-value pair', () => {
const { result } = setUp({ foo: 'bar', a: 1 });
const { result } = setUp<{ foo: string; a: number; newKey?: number }>({ foo: 'bar', a: 1 });
const [, utils] = result.current;
act(() => {
// @ts-ignore
utils.set('newKey', 99);
});
@ -65,11 +62,10 @@ it('should override current value if setting existing key', () => {
const [, utils] = result.current;
act(() => {
// @ts-ignore
utils.set('foo', 99);
utils.set('foo', 'qux');
});
expect(result.current[0]).toEqual({ foo: 99, a: 1 });
expect(result.current[0]).toEqual({ foo: 'qux', a: 1 });
});
it('should remove corresponding key-value pair for existing provided key', () => {
@ -77,7 +73,6 @@ it('should remove corresponding key-value pair for existing provided key', () =>
const [, utils] = result.current;
act(() => {
// @ts-ignore
utils.remove('foo');
});
@ -85,11 +80,10 @@ it('should remove corresponding key-value pair for existing provided key', () =>
});
it('should do nothing if removing non-existing provided key', () => {
const { result } = setUp({ foo: 'bar', a: 1 });
const { result } = setUp<{ foo: string; a: number; nonExisting?: any }>({ foo: 'bar', a: 1 });
const [, utils] = result.current;
act(() => {
// @ts-ignore
utils.remove('nonExisting');
});
@ -97,11 +91,10 @@ it('should do nothing if removing non-existing provided key', () => {
});
it('should reset map to initial object provided', () => {
const { result } = setUp({ foo: 'bar', a: 1 });
const { result } = setUp<{ foo: string; a: number; z?: number }>({ foo: 'bar', a: 1 });
const [, utils] = result.current;
act(() => {
// @ts-ignore
utils.set('z', 99);
});

View File

@ -1,5 +1,5 @@
import useNumber from '../useNumber';
import useCounter from '../useCounter';
import useNumber from '../useNumber';
it('should be an alias for useCounter', () => {
expect(useNumber).toBe(useCounter);

View File

@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { Subject } from 'rxjs';
import useObservable, { Observable } from '../useObservable';
import * as useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect';
import useObservable, { Observable } from '../useObservable';
const setUp = (observable: Observable<any>, initialValue?: any) =>
renderHook(() => useObservable(observable, initialValue));

View File

@ -0,0 +1,65 @@
import { renderHook } from '@testing-library/react-hooks';
import usePreviousDistinct from '../usePreviousDistinct';
describe('usePreviousDistinct with default compare', () => {
const hook = renderHook(props => usePreviousDistinct(props), { initialProps: 0 });
it('should return undefined on initial render', () => {
expect(hook.result.current).toBe(undefined);
});
it('should return previous state only after a different value is rendered', () => {
expect(hook.result.current).toBeUndefined();
hook.rerender(1);
expect(hook.result.current).toBe(0);
hook.rerender(2);
hook.rerender(2);
expect(hook.result.current).toBe(1);
hook.rerender(3);
expect(hook.result.current).toBe(2);
});
});
describe('usePreviousDistinct with complex comparison', () => {
const exampleObjects = [
{
id: 'something-unique',
name: 'Nancy',
},
{
id: 'something-unique2',
name: 'Fred',
},
{
id: 'something-unique3',
name: 'Bill',
},
{
id: 'something-unique4',
name: 'Alice',
},
];
const hook = renderHook(
props => usePreviousDistinct(props, (prev, next) => (prev && prev.id) === (next && next.id)),
{
initialProps: exampleObjects[0],
}
);
it('should return undefined on initial render', () => {
expect(hook.result.current).toBe(undefined);
});
it('should return previous state only after a different value is rendered', () => {
expect(hook.result.current).toBeUndefined();
hook.rerender(exampleObjects[1]);
expect(hook.result.current).toMatchObject(exampleObjects[0]);
hook.rerender(exampleObjects[2]);
hook.rerender(exampleObjects[2]);
expect(hook.result.current).toMatchObject(exampleObjects[1]);
hook.rerender(exampleObjects[3]);
expect(hook.result.current).toMatchObject(exampleObjects[2]);
});
});

View File

@ -0,0 +1,155 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { replaceRaf } from 'raf-stub';
import useRaf from '../useRaf';
/**
* New requestAnimationFrame after being replaced with raf-stub for testing purposes.
*/
interface RequestAnimationFrame {
reset(): void;
step(): void;
}
declare var requestAnimationFrame: RequestAnimationFrame;
replaceRaf();
const fixedStart = 1564949709496;
const spyDateNow = jest.spyOn(Date, 'now').mockImplementation(() => fixedStart);
beforeEach(() => {
jest.useFakeTimers();
requestAnimationFrame.reset();
});
afterEach(() => {
jest.clearAllTimers();
requestAnimationFrame.reset();
});
it('should init percentage of time elapsed', () => {
const { result } = renderHook(() => useRaf());
const timeElapsed = result.current;
expect(timeElapsed).toBe(0);
});
it('should return corresponding percentage of time elapsed for default ms', () => {
const { result } = renderHook(() => useRaf());
expect(result.current).toBe(0);
act(() => {
jest.runOnlyPendingTimers(); // start after delay
spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.25); // 25%
requestAnimationFrame.step();
});
expect(result.current).toBe(0.25);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.5); // 50%
requestAnimationFrame.step();
});
expect(result.current).toBe(0.5);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.75); // 75%
requestAnimationFrame.step();
});
expect(result.current).toBe(0.75);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + 1e12); // 100%
requestAnimationFrame.step();
});
expect(result.current).toBe(1);
});
it('should return corresponding percentage of time elapsed for custom ms', () => {
const customMs = 2000;
const { result } = renderHook(() => useRaf(customMs));
expect(result.current).toBe(0);
act(() => {
jest.runOnlyPendingTimers(); // start after delay
spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.25); // 25%
requestAnimationFrame.step();
});
expect(result.current).toBe(0.25);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.5); // 50%
requestAnimationFrame.step();
});
expect(result.current).toBe(0.5);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.75); // 75%
requestAnimationFrame.step();
});
expect(result.current).toBe(0.75);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + customMs); // 100%
requestAnimationFrame.step();
});
expect(result.current).toBe(1);
});
it('should return always 1 after corresponding ms reached', () => {
const { result } = renderHook(() => useRaf());
expect(result.current).toBe(0);
act(() => {
jest.runOnlyPendingTimers(); // start after delay
spyDateNow.mockImplementationOnce(() => fixedStart + 1e12); // 100%
requestAnimationFrame.step();
});
expect(result.current).toBe(1);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 1.1); // 110%
requestAnimationFrame.step();
});
expect(result.current).toBe(1);
act(() => {
spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 3); // 300%
requestAnimationFrame.step();
});
expect(result.current).toBe(1);
});
it('should wait until delay reached to start calculating elapsed percentage', () => {
const { result } = renderHook(() => useRaf(undefined, 500));
expect(result.current).toBe(0);
act(() => {
jest.advanceTimersByTime(250); // fast-forward only half of custom delay
});
expect(result.current).toBe(0);
act(() => {
jest.advanceTimersByTime(249); // fast-forward 1ms less than custom delay
});
expect(result.current).toBe(0);
act(() => {
jest.advanceTimersByTime(1); // fast-forward exactly to custom delay
});
expect(result.current).not.toBe(0);
});
it('should clear pending timers on unmount', () => {
const spyRafStop = jest.spyOn(global, 'cancelAnimationFrame' as any);
const { unmount } = renderHook(() => useRaf());
expect(clearTimeout).not.toHaveBeenCalled();
expect(spyRafStop).not.toHaveBeenCalled();
unmount();
expect(clearTimeout).toHaveBeenCalledTimes(2);
expect(spyRafStop).toHaveBeenCalledTimes(1);
});

View File

@ -1,23 +1,26 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { replaceRaf } from 'raf-stub';
import useRafLoop from '../useRafLoop';
declare var requestAnimationFrame: {
add: (cb: Function) => number;
remove: (id: number) => void;
flush: (duration?: number) => void;
reset: () => void;
step: (steps?: number, duration?: number) => void;
};
describe('useRafLoop', () => {
it('should be defined', () => {
expect(useRafLoop).toBeDefined();
beforeAll(() => {
replaceRaf();
});
it('should call a callback constantly inside the raf loop', done => {
let calls = 0;
const spy = () => calls++;
renderHook(() => useRafLoop(spy), { initialProps: false });
afterEach(() => {
requestAnimationFrame.reset();
});
expect(calls).toEqual(0);
setTimeout(() => {
expect(calls).toBeGreaterThanOrEqual(2);
done();
}, 120);
it('should be defined', () => {
expect(useRafLoop).toBeDefined();
});
it('should return stop function, start function and loop state', () => {
@ -28,26 +31,31 @@ describe('useRafLoop', () => {
expect(typeof hook.result.current[2]).toEqual('function');
});
it('first element call should stop the loop', done => {
let calls = 0;
const spy = () => calls++;
it('should call a callback constantly inside the raf loop', () => {
const spy = jest.fn();
renderHook(() => useRafLoop(spy), { initialProps: false });
expect(spy).not.toBeCalled();
requestAnimationFrame.step();
requestAnimationFrame.step();
expect(spy).toBeCalledTimes(2);
});
it('first element call should stop the loop', () => {
const spy = jest.fn();
const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
// stop the loop
expect(spy).not.toBeCalled();
act(() => {
hook.result.current[0]();
});
setTimeout(() => {
expect(calls).toEqual(0);
done();
}, 50);
requestAnimationFrame.step();
expect(spy).not.toBeCalled();
});
it('second element should represent loop state', done => {
let calls = 0;
const spy = () => calls++;
it('second element should represent loop state', () => {
const spy = jest.fn();
const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
expect(hook.result.current[1]).toBe(true);
@ -56,56 +64,39 @@ describe('useRafLoop', () => {
act(() => {
hook.result.current[0]();
});
expect(hook.result.current[1]).toBe(false);
setTimeout(() => {
expect(calls).toEqual(0);
done();
}, 120);
});
it('third element call should restart loop', done => {
let calls = 0;
const spy = () => calls++;
it('third element call should restart loop', () => {
const spy = jest.fn();
const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
expect(hook.result.current[1]).toBe(true);
expect(spy).not.toBeCalled();
// stop the loop
act(() => {
hook.result.current[0]();
});
requestAnimationFrame.step();
expect(spy).not.toBeCalled();
setTimeout(() => {
expect(hook.result.current[1]).toBe(false);
expect(calls).toEqual(0);
// start the loop
act(() => {
hook.result.current[2]();
});
// start the loop
act(() => {
hook.result.current[2]();
});
setTimeout(() => {
expect(hook.result.current[1]).toBe(true);
expect(calls).toBeGreaterThanOrEqual(2);
done();
}, 120);
}, 50);
requestAnimationFrame.step();
requestAnimationFrame.step();
expect(spy).toBeCalledTimes(2);
});
it('loop should stop itself on unmount', done => {
let calls = 0;
const spy = () => calls++;
it('loop should stop itself on unmount', () => {
const spy = jest.fn();
const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
hook.unmount();
setTimeout(() => {
expect(calls).toEqual(0);
requestAnimationFrame.step();
done();
}, 50);
expect(spy).not.toBeCalled();
});
});

View File

@ -40,3 +40,21 @@ it('should merge changes into current state when providing function', () => {
expect(result.current[0]).toEqual({ foo: 'bar', count: 2, someBool: true });
});
/**
* Enforces cases where a hook can safely depend on the callback without
* causing an endless rerender cycle: useEffect(() => setState({ data }), [setState]);
*/
it('should return a memoized setState callback', () => {
const { result, rerender } = setUp({ ok: false });
const [, setState1] = result.current;
act(() => {
setState1({ ok: true });
});
rerender();
const [, setState2] = result.current;
expect(setState1).toBe(setState2);
});

View File

@ -0,0 +1,119 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { Spring } from 'rebound';
import useSpring from '../useSpring';
// simulate Spring for testing
const mockSetCurrentValue = jest.fn();
const mockAddListener = jest.fn();
const mockSetEndValue = jest.fn();
const mockRemoveListener = jest.fn();
let triggerSpringUpdate = () => {};
let springListener: Listener = { onSpringUpdate: () => {} };
interface Listener {
onSpringUpdate: (currentSpring: Spring) => void;
}
const mockCreateSpring: Spring = jest.fn().mockImplementation(() => {
let currentValue = 0;
let endValue = 0;
const getCloserValue = (a, b) => Math.round((a + b) / 2);
const getCurrentValue = () => {
currentValue = getCloserValue(currentValue, endValue);
return currentValue;
};
triggerSpringUpdate = () => {
if (currentValue !== endValue) {
springListener.onSpringUpdate({ getCurrentValue } as any);
}
};
return {
setCurrentValue: val => {
currentValue = val;
mockSetCurrentValue(val);
},
addListener: newListener => {
springListener = newListener;
mockAddListener(newListener);
},
setEndValue: val => {
endValue = val;
mockSetEndValue(val);
},
removeListener: mockRemoveListener,
};
}) as any;
jest.mock('rebound', () => {
return {
Sprint: {},
SpringSystem: jest.fn().mockImplementation(() => {
return { createSpring: mockCreateSpring };
}),
};
});
it('should init value to provided target', () => {
const { result } = renderHook(() => useSpring(70));
expect(result.current).toBe(70);
expect(mockSetCurrentValue).toHaveBeenCalledTimes(1);
expect(mockSetCurrentValue).toHaveBeenCalledWith(70);
expect(mockCreateSpring).toHaveBeenCalledTimes(1);
expect(mockCreateSpring).toHaveBeenCalledWith(50, 3);
});
it('should create spring with custom tension and friction args provided', () => {
renderHook(() => useSpring(500, 20, 7));
expect(mockCreateSpring).toHaveBeenCalledTimes(1);
expect(mockCreateSpring).toHaveBeenCalledWith(20, 7);
});
it('should subscribe only once', () => {
const { rerender } = renderHook(() => useSpring());
expect(mockAddListener).toHaveBeenCalledTimes(1);
expect(mockAddListener).toHaveBeenCalledWith(springListener);
rerender();
expect(mockAddListener).toHaveBeenCalledTimes(1);
});
it('should handle spring update', () => {
let targetValue = 70;
let lastSpringValue = targetValue;
const { result, rerender } = renderHook(() => useSpring(targetValue));
targetValue = 100;
rerender();
expect(result.current).toBe(lastSpringValue);
act(() => {
triggerSpringUpdate(); // simulate new spring value
});
expect(result.current).toBeGreaterThan(lastSpringValue);
expect(result.current).toBeLessThanOrEqual(targetValue);
lastSpringValue = result.current;
act(() => {
triggerSpringUpdate(); // simulate another new spring value
});
expect(result.current).toBeGreaterThan(lastSpringValue);
expect(result.current).toBeLessThanOrEqual(targetValue);
});
it('should remove listener on unmount', () => {
const { unmount } = renderHook(() => useSpring());
expect(mockRemoveListener).not.toHaveBeenCalled();
unmount();
expect(mockRemoveListener).toHaveBeenCalledTimes(1);
expect(mockRemoveListener).toHaveBeenCalledWith(springListener);
});

View File

@ -0,0 +1,144 @@
import { renderHook, act } from '@testing-library/react-hooks';
import useStateList from '../useStateList';
const callNext = hook => {
act(() => {
const { next } = hook.result.current;
next();
});
};
const callPrev = hook => {
act(() => {
const { prev } = hook.result.current;
prev();
});
};
describe('happy flow', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: ['a', 'b', 'c'],
},
});
it('should return the first state on initial render', () => {
const { state } = hook.result.current;
expect(state).toBe('a');
});
it('should return the second state after calling the "next" function', () => {
callNext(hook);
const { state } = hook.result.current;
expect(state).toBe('b');
});
it('should return the first state again after calling the "next" function "stateSet.length" times', () => {
callNext(hook);
callNext(hook);
const { state } = hook.result.current;
expect(state).toBe('a');
});
it('should return the last state again after calling the "prev" function', () => {
callPrev(hook);
const { state } = hook.result.current;
expect(state).toBe('c');
});
it('should return the previous state after calling the "prev" function', () => {
callPrev(hook);
const { state } = hook.result.current;
expect(state).toBe('b');
});
});
describe('with empty state set', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: [],
},
});
it('should return undefined on initial render', () => {
const { state } = hook.result.current;
expect(state).toBe(undefined);
});
it('should always return undefined (calling next)', () => {
callNext(hook);
const { state } = hook.result.current;
expect(state).toBe(undefined);
});
it('should always return undefined (calling prev)', () => {
callPrev(hook);
const { state } = hook.result.current;
expect(state).toBe(undefined);
});
});
describe('with a single state set', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: ['a'],
},
});
it('should return "a" on initial render', () => {
const { state } = hook.result.current;
expect(state).toBe('a');
});
it('should always return "a" (calling next)', () => {
callNext(hook);
const { state } = hook.result.current;
expect(state).toBe('a');
});
it('should always return "a" (calling prev)', () => {
callPrev(hook);
const { state } = hook.result.current;
expect(state).toBe('a');
});
});
describe('with stateSet updates', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: ['a', 'c', 'b', 'f', 'g'],
},
});
it('should return the last element after updating with a shorter state set', () => {
// Go to the 4th state
callNext(hook); // c
callNext(hook); // b
callNext(hook); // f
// Update the state set with less elements
hook.rerender({
stateSet: ['a', 'c'],
});
const { state } = hook.result.current;
expect(state).toBe('c');
});
it('should return the element in the same position after updating with a larger state set', () => {
hook.rerender({
stateSet: ['a', 'f', 'l'],
});
const { state } = hook.result.current;
expect(state).toBe('f');
});
});

View File

@ -0,0 +1,136 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useTimeout } from '../index';
import { UseTimeoutReturn } from '../useTimeout';
beforeAll(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('should be defined', () => {
expect(useTimeout).toBeDefined();
});
it('should return three functions', () => {
const hook = renderHook(() => useTimeout(5));
expect(hook.result.current.length).toBe(3);
expect(typeof hook.result.current[0]).toBe('function');
expect(typeof hook.result.current[1]).toBe('function');
expect(typeof hook.result.current[2]).toBe('function');
});
function getHook(ms: number = 5): [jest.Mock, RenderHookResult<{ delay: number }, UseTimeoutReturn>] {
const spy = jest.fn();
return [
spy,
renderHook(
({ delay = 5 }) => {
spy();
return useTimeout(delay);
},
{ initialProps: { delay: ms } }
),
];
}
it('should re-render component after given amount of time', done => {
const [spy, hook] = getHook();
expect(spy).toHaveBeenCalledTimes(1);
hook.waitForNextUpdate().then(() => {
expect(spy).toHaveBeenCalledTimes(2);
done();
});
jest.advanceTimersByTime(5);
});
it('should cancel timeout on unmount', () => {
const [spy, hook] = getHook();
expect(spy).toHaveBeenCalledTimes(1);
hook.unmount();
jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
});
it('first function should return actual state of timeout', done => {
let [, hook] = getHook();
let [isReady] = hook.result.current;
expect(isReady()).toBe(false);
hook.unmount();
expect(isReady()).toBe(null);
[, hook] = getHook();
[isReady] = hook.result.current;
hook.waitForNextUpdate().then(() => {
expect(isReady()).toBe(true);
done();
});
jest.advanceTimersByTime(5);
});
it('second function should cancel timeout', () => {
const [spy, hook] = getHook();
const [isReady, cancel] = hook.result.current;
expect(spy).toHaveBeenCalledTimes(1);
expect(isReady()).toBe(false);
act(() => {
cancel();
});
jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
expect(isReady()).toBe(null);
});
it('third function should reset timeout', done => {
const [spy, hook] = getHook();
const [isReady, cancel, reset] = hook.result.current;
expect(isReady()).toBe(false);
act(() => {
cancel();
});
jest.advanceTimersByTime(5);
expect(isReady()).toBe(null);
act(() => {
reset();
});
expect(isReady()).toBe(false);
hook.waitForNextUpdate().then(() => {
expect(spy).toHaveBeenCalledTimes(2);
expect(isReady()).toBe(true);
done();
});
jest.advanceTimersByTime(5);
});
it('should reset timeout on delay change', done => {
const [spy, hook] = getHook(15);
expect(spy).toHaveBeenCalledTimes(1);
hook.rerender({ delay: 5 });
hook.waitForNextUpdate().then(() => {
expect(spy).toHaveBeenCalledTimes(3);
done();
});
jest.advanceTimersByTime(15);
});

View File

@ -0,0 +1,116 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useTimeoutFn } from '../index';
import { UseTimeoutFnReturn } from '../useTimeoutFn';
describe('useTimeoutFn', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('should be defined', () => {
expect(useTimeoutFn).toBeDefined();
});
it('should return three functions', () => {
const hook = renderHook(() => useTimeoutFn(() => {}, 5));
expect(hook.result.current.length).toBe(3);
expect(typeof hook.result.current[0]).toBe('function');
expect(typeof hook.result.current[1]).toBe('function');
expect(typeof hook.result.current[2]).toBe('function');
});
function getHook(ms: number = 5): [jest.Mock, RenderHookResult<{ delay: number }, UseTimeoutFnReturn>] {
const spy = jest.fn();
return [spy, renderHook(({ delay = 5 }) => useTimeoutFn(spy, delay), { initialProps: { delay: ms } })];
}
it('should call passed function after given amount of time', () => {
const [spy] = getHook();
expect(spy).not.toHaveBeenCalled();
jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
});
it('should cancel function call on unmount', () => {
const [spy, hook] = getHook();
expect(spy).not.toHaveBeenCalled();
hook.unmount();
jest.advanceTimersByTime(5);
expect(spy).not.toHaveBeenCalled();
});
it('first function should return actual state of timeout', () => {
let [, hook] = getHook();
let [isReady] = hook.result.current;
expect(isReady()).toBe(false);
hook.unmount();
expect(isReady()).toBe(null);
[, hook] = getHook();
[isReady] = hook.result.current;
jest.advanceTimersByTime(5);
expect(isReady()).toBe(true);
});
it('second function should cancel timeout', () => {
const [spy, hook] = getHook();
const [isReady, cancel] = hook.result.current;
expect(spy).not.toHaveBeenCalled();
expect(isReady()).toBe(false);
act(() => {
cancel();
});
jest.advanceTimersByTime(5);
expect(spy).not.toHaveBeenCalled();
expect(isReady()).toBe(null);
});
it('third function should reset timeout', () => {
const [spy, hook] = getHook();
const [isReady, cancel, reset] = hook.result.current;
expect(isReady()).toBe(false);
act(() => {
cancel();
});
jest.advanceTimersByTime(5);
expect(isReady()).toBe(null);
act(() => {
reset();
});
expect(isReady()).toBe(false);
jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
expect(isReady()).toBe(true);
});
it('should reset timeout on delay change', () => {
const [spy, hook] = getHook(50);
expect(spy).not.toHaveBeenCalled();
hook.rerender({ delay: 5 });
jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,56 @@
import { renderHook } from '@testing-library/react-hooks';
import { easing } from 'ts-easing';
import * as useRaf from '../useRaf';
import useTween from '../useTween';
let spyUseRaf;
let spyEasingInCirc;
let spyEasingOutCirc;
beforeEach(() => {
spyUseRaf = jest.spyOn(useRaf, 'default').mockReturnValue(17);
spyEasingInCirc = jest.spyOn(easing, 'inCirc').mockReturnValue(999999);
spyEasingOutCirc = jest.spyOn(easing, 'outCirc').mockReturnValue(101010);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should init corresponding utils with default values', () => {
const { result } = renderHook(() => useTween());
expect(result.current).toBe(999999);
expect(spyEasingInCirc).toHaveBeenCalledTimes(1);
expect(spyEasingInCirc).toHaveBeenCalledWith(17);
expect(spyUseRaf).toHaveBeenCalledTimes(1);
expect(spyUseRaf).toHaveBeenCalledWith(200, 0);
});
it('should init corresponding utils with custom values', () => {
const { result } = renderHook(() => useTween('outCirc', 500, 15));
expect(result.current).toBe(101010);
expect(spyEasingOutCirc).toHaveBeenCalledTimes(1);
expect(spyEasingOutCirc).toHaveBeenCalledWith(17);
expect(spyUseRaf).toHaveBeenCalledTimes(1);
expect(spyUseRaf).toHaveBeenCalledWith(500, 15);
});
describe('when invalid easing name is provided', () => {
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(console, 'trace').mockImplementation(() => {});
});
it('should log an error', () => {
const { result } = renderHook(() => useTween('grijanderl'));
expect(result.current).toBe(0);
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('useTween() expected "easingName" property to be a valid easing function name')
);
expect(console.trace).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,26 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useUpdate from '../useUpdate';
it('should init update function', () => {
const { result } = renderHook(() => useUpdate());
const update = result.current;
expect(update).toBeInstanceOf(Function);
});
it('should forces a re-render every time update function is called', () => {
let renderCount = 0;
const { result } = renderHook(() => {
renderCount++;
return useUpdate();
});
const update = result.current;
expect(renderCount).toBe(1);
act(() => update());
expect(renderCount).toBe(2);
act(() => update());
expect(renderCount).toBe(3);
});

View File

@ -0,0 +1,74 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useUpsert from '../useUpsert';
interface TestItem {
id: string;
text: string;
}
const testItems: TestItem[] = [{ id: '1', text: '1' }, { id: '2', text: '2' }];
const itemsAreEqual = (a: TestItem, b: TestItem) => {
return a.id === b.id;
};
const setUp = (initialList: TestItem[] = []) => renderHook(() => useUpsert<TestItem>(itemsAreEqual, initialList));
describe('useUpsert', () => {
describe('initialization', () => {
const { result } = setUp(testItems);
const [list, utils] = result.current;
it('properly initiates the list content', () => {
expect(list).toEqual(testItems);
});
it('returns an upsert function', () => {
expect(utils.upsert).toBeInstanceOf(Function);
});
});
describe('upserting a new item', () => {
const { result } = setUp(testItems);
const [, utils] = result.current;
const newItem: TestItem = {
id: '3',
text: '3',
};
act(() => {
utils.upsert(newItem);
});
it('inserts a new item', () => {
expect(result.current[0]).toContain(newItem);
});
it('works immutably', () => {
expect(result.current[0]).not.toBe(testItems);
});
});
describe('upserting an existing item', () => {
const { result } = setUp(testItems);
const [, utils] = result.current;
const newItem: TestItem = {
id: '2',
text: '4',
};
act(() => {
utils.upsert(newItem);
});
const updatedList = result.current[0];
it('has the same length', () => {
expect(updatedList).toHaveLength(testItems.length);
});
it('updates the item', () => {
expect(updatedList).toContain(newItem);
});
it('works immutably', () => {
expect(updatedList).not.toBe(testItems);
});
});
});

View File

@ -23,6 +23,7 @@ export { default as useFullscreen } from './useFullscreen';
export { default as useGeolocation } from './useGeolocation';
export { default as useGetSet } from './useGetSet';
export { default as useGetSetState } from './useGetSetState';
export { default as useHarmonicIntervalFn } from './useHarmonicIntervalFn';
export { default as useHover } from './useHover';
export { default as useHoverDirty } from './useHoverDirty';
export { default as useIdle } from './useIdle';
@ -54,9 +55,13 @@ export { default as useOrientation } from './useOrientation';
export { default as usePageLeave } from './usePageLeave';
export { default as usePermission } from './usePermission';
export { default as usePrevious } from './usePrevious';
export { default as usePreviousDistinct } from './usePreviousDistinct';
export { default as usePromise } from './usePromise';
export { default as useRaf } from './useRaf';
export { default as useRafLoop } from './useRafLoop';
/**
* @deprecated This hook is obsolete, use `useMountedState` instead
*/
export { default as useRefMounted } from './useRefMounted';
export { default as useScroll } from './useScroll';
export { default as useScrolling } from './useScrolling';
@ -67,15 +72,18 @@ export { default as useSpeech } from './useSpeech';
// not exported because of peer dependency
// export { default as useSpring } from './useSpring';
export { default as useStartTyping } from './useStartTyping';
export { default as useStateList } from './useStateList';
export { default as useThrottle } from './useThrottle';
export { default as useThrottleFn } from './useThrottleFn';
export { default as useTimeout } from './useTimeout';
export { default as useTimeoutFn } from './useTimeoutFn';
export { default as useTitle } from './useTitle';
export { default as useToggle } from './useToggle';
export { default as useTween } from './useTween';
export { default as useUnmount } from './useUnmount';
export { default as useUpdate } from './useUpdate';
export { default as useUpdateEffect } from './useUpdateEffect';
export { default as useUpsert } from './useUpsert';
export { default as useVideo } from './useVideo';
export { useWait, Waiter } from './useWait';
export { default as useWindowScroll } from './useWindowScroll';

View File

@ -1,56 +1,85 @@
import { useEffect, useState } from 'react';
import * as React from 'react';
import isEqual from 'react-fast-compare';
import { off, on } from './util';
export interface BatterySensorState {
const { useState, useEffect } = React;
export interface BatteryState {
charging: boolean;
level: number;
chargingTime: number;
dischargingTime: number;
level: number;
}
const useBattery = () => {
const [state, setState] = useState({});
let mounted = true;
let battery: any = null;
interface BatteryManager extends Readonly<BatteryState>, EventTarget {
onchargingchange: () => void;
onchargingtimechange: () => void;
ondischargingtimechange: () => void;
onlevelchange: () => void;
}
const onChange = () => {
const { charging, level, chargingTime, dischargingTime } = battery;
setState({
charging,
level,
chargingTime,
dischargingTime,
});
};
interface NavigatorWithPossibleBattery extends Navigator {
getBattery?: () => Promise<BatteryManager>;
}
const onBattery = () => {
onChange();
on(battery, 'chargingchange', onChange);
on(battery, 'levelchange', onChange);
on(battery, 'chargingtimechange', onChange);
on(battery, 'dischargingtimechange', onChange);
};
type UseBatteryState =
| { isSupported: false } // Battery API is not supported
| { isSupported: true; fetched: false } // battery API supported but not fetched yet
| BatteryState & { isSupported: true; fetched: true }; // battery API supported and fetched
const nav: NavigatorWithPossibleBattery | undefined = typeof navigator === 'object' ? navigator : undefined;
const isBatteryApiSupported = nav && typeof nav.getBattery === 'function';
function useBatteryMock(): UseBatteryState {
return { isSupported: false };
}
function useBattery(): UseBatteryState {
const [state, setState] = useState<UseBatteryState>({ isSupported: true, fetched: false });
useEffect(() => {
(navigator as any).getBattery().then((bat: any) => {
if (mounted) {
battery = bat;
onBattery();
let isMounted = true;
let battery: BatteryManager | null = null;
const handleChange = () => {
if (!isMounted || !battery) {
return;
}
const newState: UseBatteryState = {
isSupported: true,
fetched: true,
level: battery.level,
charging: battery.charging,
dischargingTime: battery.dischargingTime,
chargingTime: battery.chargingTime,
};
!isEqual(state, newState) && setState(newState);
};
nav!.getBattery!().then((bat: BatteryManager) => {
if (!isMounted) {
return;
}
battery = bat;
on(battery, 'chargingchange', handleChange);
on(battery, 'chargingtimechange', handleChange);
on(battery, 'dischargingtimechange', handleChange);
on(battery, 'levelchange', handleChange);
handleChange();
});
return () => {
mounted = false;
isMounted = false;
if (battery) {
off(battery, 'chargingchange', onChange);
off(battery, 'levelchange', onChange);
off(battery, 'chargingtimechange', onChange);
off(battery, 'dischargingtimechange', onChange);
off(battery, 'chargingchange', handleChange);
off(battery, 'chargingtimechange', handleChange);
off(battery, 'dischargingtimechange', handleChange);
off(battery, 'levelchange', handleChange);
}
};
}, []);
return state;
};
}
export default useBattery;
export default isBatteryApiSupported ? useBattery : useBatteryMock;

View File

@ -3,11 +3,13 @@ import { isClient } from './util';
export interface ListenerType1 {
addEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
removeEventListener(name: string, handler: (event?: any) => void);
}
export interface ListenerType2 {
on(name: string, handler: (event?: any) => void, ...args: any[]);
off(name: string, handler: (event?: any) => void);
}

View File

@ -0,0 +1,20 @@
import { useEffect, useRef } from 'react';
import { setHarmonicInterval, clearHarmonicInterval } from 'set-harmonic-interval';
const useHarmonicIntervalFn = (fn: Function, delay: number | null = 0) => {
const latestCallback = useRef<Function>(() => {});
useEffect(() => {
latestCallback.current = fn;
});
useEffect(() => {
if (delay !== null) {
const interval = setHarmonicInterval(() => latestCallback.current(), delay);
return () => clearHarmonicInterval(interval);
}
return undefined;
}, [delay]);
};
export default useHarmonicIntervalFn;

View File

@ -1,4 +1,4 @@
import { useRef, useEffect } from 'react';
import { useEffect, useRef } from 'react';
const useInterval = (callback: Function, delay?: number | null) => {
const latestCallback = useRef<Function>(() => {});

View File

@ -4,6 +4,7 @@ import useEvent, { UseEventTarget } from './useEvent';
export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type KeyFilter = null | undefined | string | ((event: KeyboardEvent) => boolean);
export type Handler = (event: KeyboardEvent) => void;
export interface UseKeyOptions {
event?: 'keydown' | 'keypress' | 'keyup';
target?: UseEventTarget;

View File

@ -1,34 +1,58 @@
import { useEffect } from 'react';
import { RefObject, useEffect, useRef } from 'react';
let counter = 0;
let originalOverflow: string | null = null;
const lock = () => {
originalOverflow = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = 'hidden';
};
const unlock = () => {
document.body.style.overflow = originalOverflow;
originalOverflow = null;
};
const increment = () => {
counter++;
if (counter === 1) {
lock();
export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | null): HTMLElement | null {
if (!el) {
return null;
} else if (el.tagName === 'BODY') {
return el as HTMLElement;
} else if (el.tagName === 'IFRAME') {
const document = (el as HTMLIFrameElement).contentDocument;
return document ? document.body : null;
} else if (!(el as HTMLElement).offsetParent) {
return null;
}
};
const decrement = () => {
counter--;
if (counter === 0) {
unlock();
}
};
return getClosestBody((el as HTMLElement).offsetParent!);
}
const useLockBodyScroll = (enabled: boolean = true) => {
useEffect(() => (enabled ? (increment(), decrement) : undefined), [enabled]);
};
export interface BodyInfoItem {
counter: number;
initialOverflow: string | null;
}
export default useLockBodyScroll;
const bodies: Map<HTMLElement, BodyInfoItem> = new Map();
const doc: Document | undefined = typeof document === 'object' ? document : undefined;
export default !doc
? function useLockBodyMock(_locked: boolean = true, _elementRef?: RefObject<HTMLElement>) {}
: function useLockBody(locked: boolean = true, elementRef?: RefObject<HTMLElement>) {
elementRef = elementRef || useRef(doc!.body);
useEffect(() => {
const body = getClosestBody(elementRef!.current);
if (!body) {
return;
}
const bodyInfo = bodies.get(body);
if (locked) {
if (!bodyInfo) {
bodies.set(body, { counter: 1, initialOverflow: body.style.overflow });
body.style.overflow = 'hidden';
} else {
bodies.set(body, { counter: bodyInfo.counter + 1, initialOverflow: bodyInfo.initialOverflow });
}
} else {
if (bodyInfo) {
if (bodyInfo.counter === 1) {
bodies.delete(body);
body.style.overflow = bodyInfo.initialOverflow;
} else {
bodies.set(body, { counter: bodyInfo.counter - 1, initialOverflow: bodyInfo.initialOverflow });
}
}
}
}, [locked, elementRef.current]);
};

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback } from 'react';
import { useCallback, useEffect, useRef } from 'react';
export default function useMountedState(): () => boolean {
const mountedRef = useRef<boolean>(false);

View File

@ -0,0 +1,19 @@
import { useRef } from 'react';
function strictEquals<T>(prev: T | undefined, next: T) {
return prev === next;
}
export default function usePreviousDistinct<T>(
value: T,
compare: (prev: T | undefined, next: T) => boolean = strictEquals
) {
const prevRef = useRef<T>();
const curRef = useRef<T>();
if (!compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}

View File

@ -1,5 +1,8 @@
import { RefObject, useEffect, useRef } from 'react';
/**
* @deprecated This hook is obsolete, use `useMountedState` instead
*/
const useRefMounted = (): RefObject<boolean> => {
const refMounted = useRef<boolean>(false);
@ -14,4 +17,7 @@ const useRefMounted = (): RefObject<boolean> => {
return refMounted;
};
/**
* @deprecated This hook is obsolete, use `useMountedState` instead
*/
export default useRefMounted;

View File

@ -1,12 +1,15 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
const useSetState = <T extends object>(
initialState: T = {} as T
): [T, (patch: Partial<T> | ((prevState: T) => Partial<T>)) => void] => {
const [state, set] = useState<T>(initialState);
const setState = patch => {
set(prevState => Object.assign({}, prevState, patch instanceof Function ? patch(prevState) : patch));
};
const setState = useCallback(
patch => {
set(prevState => Object.assign({}, prevState, patch instanceof Function ? patch(prevState) : patch));
},
[set]
);
return [state, setState];
};

View File

@ -6,6 +6,7 @@ const { useState, useEffect, useRef } = React;
const DRAF = (callback: () => void) => setTimeout(callback, 35);
export type Element = ((state: State) => React.ReactElement<any>) | React.ReactElement<any>;
export interface State {
width: number;
height: number;

View File

@ -1,31 +1,37 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Spring, SpringSystem } from 'rebound';
const useSpring = (targetValue: number = 0, tension: number = 50, friction: number = 3) => {
const [spring, setSpring] = useState<Spring | null>(null);
const [value, setValue] = useState<number>(targetValue);
useEffect(() => {
const listener = {
// memoize listener to being able to unsubscribe later properly, otherwise
// listener fn will be different on each re-render and wouldn't unsubscribe properly.
const listener = useMemo(
() => ({
onSpringUpdate: currentSpring => {
const newValue = currentSpring.getCurrentValue();
setValue(newValue);
},
};
}),
[]
);
useEffect(() => {
if (!spring) {
const newSpring = new SpringSystem().createSpring(tension, friction);
newSpring.setCurrentValue(targetValue);
setSpring(newSpring);
newSpring.addListener(listener);
return;
}
return () => {
spring.removeListener(listener);
setSpring(null);
if (spring) {
spring.removeListener(listener);
setSpring(null);
}
};
}, [tension, friction]);
}, [tension, friction, spring]);
useEffect(() => {
if (spring) {

33
src/useStateList.ts Normal file
View File

@ -0,0 +1,33 @@
import { useState, useCallback } from 'react';
import useUpdateEffect from './useUpdateEffect';
export default function useStateList<T>(stateSet: T[] = []): { state: T; next: () => void; prev: () => void } {
const [currentIndex, setCurrentIndex] = useState(0);
// In case we receive a different state set, check if the current index still exists and
// reset it to the last if it don't.
useUpdateEffect(() => {
if (!stateSet[currentIndex]) {
setCurrentIndex(stateSet.length - 1);
}
}, [stateSet]);
const next = useCallback(() => {
const nextStateIndex = stateSet.length === currentIndex + 1 ? 0 : currentIndex + 1;
setCurrentIndex(nextStateIndex);
}, [stateSet, currentIndex]);
const prev = useCallback(() => {
const prevStateIndex = currentIndex === 0 ? stateSet.length - 1 : currentIndex - 1;
setCurrentIndex(prevStateIndex);
}, [stateSet, currentIndex]);
return {
state: stateSet[currentIndex],
next,
prev,
};
}

View File

@ -1,19 +1,10 @@
import { useEffect, useState } from 'react';
import useTimeoutFn from './useTimeoutFn';
import useUpdate from './useUpdate';
const useTimeout = (ms: number = 0) => {
const [ready, setReady] = useState(false);
export type UseTimeoutReturn = [() => boolean | null, () => void, () => void];
useEffect(() => {
const timer = setTimeout(() => {
setReady(true);
}, ms);
export default function useTimeout(ms: number = 0): UseTimeoutReturn {
const update = useUpdate();
return () => {
clearTimeout(timer);
};
}, [ms]);
return ready;
};
export default useTimeout;
return useTimeoutFn(update, ms);
}

29
src/useTimeoutFn.ts Normal file
View File

@ -0,0 +1,29 @@
import { useCallback, useEffect, useRef } from 'react';
export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
export default function useTimeoutFn(fn: Function, ms: number = 0): UseTimeoutFnReturn {
const ready = useRef<boolean | null>(false);
const timeout = useRef<ReturnType<typeof setTimeout>>();
const isReady = useCallback(() => ready.current, []);
const set = useCallback(() => {
ready.current = false;
timeout.current = setTimeout(() => {
ready.current = true;
fn();
}, ms);
}, [ms, fn]);
const clear = useCallback(() => {
ready.current = null;
timeout.current && clearTimeout(timeout.current);
}, []);
useEffect(() => {
set();
return clear;
}, [ms]);
return [isReady, clear, set];
}

39
src/useUpsert.ts Normal file
View File

@ -0,0 +1,39 @@
import useList, { Actions as ListActions } from './useList';
export interface Actions<T> extends ListActions<T> {
upsert: (item: T) => void;
}
const useUpsert = <T>(
comparisonFunction: (upsertedItem: T, existingItem: T) => boolean,
initialList: T[] = []
): [T[], Actions<T>] => {
const [items, actions] = useList(initialList);
const upsert = (upsertedItem: T) => {
let itemWasFound = false;
for (let i = 0; i < items.length; i++) {
const existingItem = items[i];
const shouldUpdate = comparisonFunction(existingItem, upsertedItem);
if (shouldUpdate) {
actions.updateAt(i, upsertedItem);
itemWasFound = true;
break;
}
}
if (!itemWasFound) {
actions.push(upsertedItem);
}
};
return [
items,
{
...actions,
upsert,
},
];
};
export default useUpsert;

View File

@ -22,6 +22,14 @@
"no-empty-interface": [false],
"object-literal-sort-keys": false,
"no-unused-expression": false,
"variable-name": [
true,
"ban-keywords",
"check-format",
"allow-pascal-case",
"allow-leading-underscore",
"allow-trailing-underscore"
],
"prettier": [
true,
{

View File

@ -1948,10 +1948,10 @@
resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89"
integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==
"@types/jest@24.0.17":
version "24.0.17"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.17.tgz#b66ea026efb746eb5db1356ee28518aaff7af416"
integrity sha512-1cy3xkOAfSYn78dsBWy4M3h/QF/HeWPchNFDjysVtp3GHeTdSmtluNnELfCmfNRRHo0OWEcpf+NsEJQvwQfdqQ==
"@types/jest@24.0.18":
version "24.0.18"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.18.tgz#9c7858d450c59e2164a8a9df0905fc5091944498"
integrity sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ==
dependencies:
"@types/jest-diff" "*"
@ -7735,10 +7735,10 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
lint-staged@9.2.3:
version "9.2.3"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-9.2.3.tgz#a4ef2b7033f83e8dbc718556610e20e0098356c0"
integrity sha512-ovDmF0c0VJDTP0VmwLetJQ+lVGyNqOkTniwO9S0MOJxGxIExpSRTL56/ZmvXZ1tHNA53GBbXQbfS8RnNGRXFjg==
lint-staged@9.2.4:
version "9.2.4"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-9.2.4.tgz#7a35839c9c6103ec98eabf43e35657c209509345"
integrity sha512-RAt5ogbLRiKy9+T3dKbbPKP6tN2I9VjEtTXQFxdkauUdMjIBsS70ikwwSgsBR8xVVCUSX7sVDyWCyem5xB8YDw==
dependencies:
chalk "^2.4.2"
commander "^2.20.0"
@ -9973,6 +9973,11 @@ raf-schd@^4.0.0:
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf-stub@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/raf-stub/-/raf-stub-3.0.0.tgz#40e53dc3ad3b241311f914bbd41dc11a2c9ee0a9"
integrity sha512-64wjDTI8NAkplC3WYF3DUBXmdx8AZF0ubxiicZi83BKW5hcdvMtbwDe6gpFBngTo6+XIJbfwmUP8lMa85UPK6A==
raf@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
@ -10135,6 +10140,11 @@ react-focus-lock@^1.18.3:
prop-types "^15.6.2"
react-clientside-effect "^1.2.0"
react-frame-component@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.1.1.tgz#ea8f7c518ef6b5ad72146dd1f648752369826894"
integrity sha512-NfJp90AvYA1R6+uSYafQ+n+UM2HjHqi4WGHeprVXa6quU9d8o6ZFRzQ36uemY82dlkZFzf2jigFx6E4UzNFajA==
react-helmet-async@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.0.2.tgz#bb55dd8268f7b15aac69c6b22e2f950abda8cc44"
@ -10943,10 +10953,10 @@ select@^1.1.2:
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
semantic-release@15.13.21:
version "15.13.21"
resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-15.13.21.tgz#d021c75f889cff75ae3410736942bee6c4557da7"
integrity sha512-3S9thQas28iv3NeHUqQVsDnxMcBGQICdxabeNnJ8BnbRBvCkgqCg3v9zo/+O5a8GCyxrgjtwJ2iWozL8SiIq1w==
semantic-release@15.13.24:
version "15.13.24"
resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-15.13.24.tgz#f0b9544427d059ba5e3c89ac1545234130796be7"
integrity sha512-OPshm6HSp+KmZP9dUv1o3MRILDgOeHYWPI+XSpQRERMri7QkaEiIPkZzoNm2d6KDeFDnp03GphQQS4+Zfo+x/Q==
dependencies:
"@semantic-release/commit-analyzer" "^6.1.0"
"@semantic-release/error" "^2.2.0"
@ -10973,7 +10983,7 @@ semantic-release@15.13.21:
resolve-from "^5.0.0"
semver "^6.0.0"
signale "^1.2.1"
yargs "^13.1.0"
yargs "^14.0.0"
semver-compare@^1.0.0:
version "1.0.0"
@ -11057,6 +11067,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-harmonic-interval@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.0.tgz#2759658395579fc336b9cd026eefe1ad9e742f9b"
integrity sha512-RJtrhB5G10e5A1auBv/jHGq0KWfHH8PAb4ln4+kjiHS46aAP12rSdarSj9GDlxQ3QULA2pefEDpm9Y1Xnz+eng==
set-value@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
@ -12155,10 +12170,10 @@ tslint-react@4.0.0:
dependencies:
tsutils "^3.9.1"
tslint@5.18.0:
version "5.18.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6"
integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==
tslint@5.19.0:
version "5.19.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.19.0.tgz#a2cbd4a7699386da823f6b499b8394d6c47bb968"
integrity sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==
dependencies:
"@babel/code-frame" "^7.0.0"
builtin-modules "^1.1.1"
@ -12881,7 +12896,7 @@ yargs@^11.0.0:
y18n "^3.2.1"
yargs-parser "^9.0.2"
yargs@^13.1.0, yargs@^13.3.0:
yargs@^13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
@ -12897,6 +12912,23 @@ yargs@^13.1.0, yargs@^13.3.0:
y18n "^4.0.0"
yargs-parser "^13.1.1"
yargs@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.0.0.tgz#ba4cacc802b3c0b3e36a9e791723763d57a85066"
integrity sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow==
dependencies:
cliui "^5.0.0"
decamelize "^1.2.0"
find-up "^3.0.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^3.0.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.1.1"
yn@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.0.0.tgz#0073c6b56e92aed652fbdfd62431f2d6b9a7a091"