mirror of
https://github.com/streamich/react-use.git
synced 2026-01-18 14:06:52 +00:00
Merge branch 'original-master' into animation-block-tests
# Conflicts: # package.json # yarn.lock
This commit is contained in:
commit
86355db002
@ -7,8 +7,12 @@ end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 80
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 120
|
||||
ij_typescript_enforce_trailing_comma = keep
|
||||
ij_typescript_use_double_quotes = false
|
||||
ij_typescript_force_quote_style = true
|
||||
ij_typescript_align_imports = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = 0
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report if you having any problems using the package
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What is the current behavior?**
|
||||
|
||||
**Steps to reproduce it and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have extra dependencies other than `react-use`. Paste the link to your [JSFiddle](https://jsfiddle.net) or [CodeSandbox](https://codesandbox.io) example below:**
|
||||
|
||||
**What is the expected behavior?**
|
||||
|
||||
**A little about versions:**
|
||||
- _OS_:
|
||||
- _Browser (vendor and version)_:
|
||||
- _React_:
|
||||
- _`react-use`_:
|
||||
- _Did this worked in the previous package version?_
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Have an idea? Great! Let us know, maybe we`ve been waiting only for you =)
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
67
CHANGELOG.md
67
CHANGELOG.md
@ -1,3 +1,70 @@
|
||||
# [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return from useUpdateEffect ([2f70dc2](https://github.com/streamich/react-use/commit/2f70dc2))
|
||||
* useUpdateEffect add return ([8b24df4](https://github.com/streamich/react-use/commit/8b24df4))
|
||||
|
||||
## [10.6.3](https://github.com/streamich/react-use/compare/v10.6.2...v10.6.3) (2019-08-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow import default for ESM ([bd3a062](https://github.com/streamich/react-use/commit/bd3a062))
|
||||
* rollup build error: Cannot call a namespace ('writeText') ([b3e672b](https://github.com/streamich/react-use/commit/b3e672b))
|
||||
|
||||
## [10.6.2](https://github.com/streamich/react-use/compare/v10.6.1...v10.6.2) (2019-08-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 fix master ([d1df7a5](https://github.com/streamich/react-use/commit/d1df7a5))
|
||||
|
||||
## [10.6.1](https://github.com/streamich/react-use/compare/v10.6.0...v10.6.1) (2019-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* useUpdateEffect run on the wrong time ([1d5cd10](https://github.com/streamich/react-use/commit/1d5cd10))
|
||||
|
||||
# [10.6.0](https://github.com/streamich/react-use/compare/v10.5.0...v10.6.0) (2019-08-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add min/max to useNumber ([586faab](https://github.com/streamich/react-use/commit/586faab))
|
||||
|
||||
# [10.5.0](https://github.com/streamich/react-use/compare/v10.4.0...v10.5.0) (2019-08-03)
|
||||
|
||||
|
||||
|
||||
@ -127,7 +127,7 @@
|
||||
- [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`.
|
||||
- [`useSetState`](./docs/useSetState.md) — creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0)
|
||||
- [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean.
|
||||
- [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number.
|
||||
- [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo)
|
||||
- [`useList`](./docs/useList.md) — tracks state of an array.
|
||||
- [`useMap`](./docs/useMap.md) — tracks state of an object.
|
||||
|
||||
|
||||
@ -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>: <span>supported</span> <br />
|
||||
<strong>Battery state</strong>: <span>fetched</span> <br />
|
||||
<strong>Charge level</strong>: <span>{ (batteryState.level * 100).toFixed(0) }%</span> <br />
|
||||
<strong>Charging</strong>: <span>{ batteryState.charging ? 'yes' : 'no' }</span> <br />
|
||||
<strong>Charging time</strong>:
|
||||
<span>{ batteryState.chargingTime ? batteryState.chargingTime : 'finished' }</span> <br />
|
||||
<strong>Discharging time</strong>: <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.
|
||||
|
||||
@ -11,19 +11,50 @@ React state hook that tracks a numeric value.
|
||||
import {useCounter, useNumber} from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const [value, {inc, dec, get, set, reset}] = useCounter(5);
|
||||
const [min, { inc: incMin, dec: decMin }] = useCounter(1);
|
||||
const [max, { inc: incMax, dec: decMax }] = useCounter(10);
|
||||
const [value, { inc, dec, set, reset }] = useCounter(5, max, min);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{value} is {get()}</div>
|
||||
<button onClick={() => inc()}>Increment</button>
|
||||
<button onClick={() => dec()}>Decrement</button>
|
||||
<button onClick={() => inc(5)}>Increment (+5)</button>
|
||||
<button onClick={() => dec(5)}>Decrement (-5)</button>
|
||||
<button onClick={() => set(100)}>Set 100</button>
|
||||
<button onClick={() => reset()}>Reset</button>
|
||||
<button onClick={() => reset(25)}>Reset (25)</button>
|
||||
<div>
|
||||
current: { value } [min: { min }; max: { max }]
|
||||
</div>
|
||||
|
||||
<br />
|
||||
Current value: <button onClick={ () => inc() }>Increment</button>
|
||||
<button onClick={ () => dec() }>Decrement</button>
|
||||
<button onClick={ () => inc(5) }>Increment (+5)</button>
|
||||
<button onClick={ () => dec(5) }>Decrement (-5)</button>
|
||||
<button onClick={ () => set(100) }>Set 100</button>
|
||||
<button onClick={ () => reset() }>Reset</button>
|
||||
<button onClick={ () => reset(25) }>Reset (25)</button>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
Min value:
|
||||
<button onClick={ () => incMin() }>Increment</button>
|
||||
<button onClick={ () => decMin() }>Decrement</button>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
Max value:
|
||||
<button onClick={ () => incMax() }>Increment</button>
|
||||
<button onClick={ () => decMax() }>Decrement</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
```ts
|
||||
const [ current, { inc, dec, get, set, reset } ] = useCounter(initial: number, max: number | null = null, 20: number | null = null);
|
||||
```
|
||||
- `current` - current counter value;
|
||||
- `get(): number` - getter of current counter value;
|
||||
- `inc(delta: number): void` - increment current value;
|
||||
- `dec(delta: number): void` - decrement current value;
|
||||
- `set(value: number): void` - set arbitrary value;
|
||||
- `reset(value: number): void` - as the `set`, but also will assign value by reference to the `initial` parameter;
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
28
docs/useUpsert.md
Normal file
28
docs/useUpsert.md
Normal 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>
|
||||
);
|
||||
};
|
||||
```
|
||||
44
package.json
44
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-use",
|
||||
"version": "10.5.0",
|
||||
"version": "10.8.0",
|
||||
"description": "Collection of React Hooks",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -67,39 +67,39 @@
|
||||
"@semantic-release/changelog": "3.0.4",
|
||||
"@semantic-release/git": "7.0.16",
|
||||
"@semantic-release/npm": "5.1.13",
|
||||
"@storybook/addon-actions": "5.1.10",
|
||||
"@storybook/addon-knobs": "5.1.10",
|
||||
"@storybook/addon-notes": "5.1.10",
|
||||
"@storybook/addon-options": "5.1.10",
|
||||
"@storybook/react": "5.1.10",
|
||||
"@testing-library/react-hooks": "^1.1.0",
|
||||
"@types/jest": "24.0.17",
|
||||
"@types/react": "16.8.23",
|
||||
"@storybook/addon-actions": "5.1.11",
|
||||
"@storybook/addon-knobs": "5.1.11",
|
||||
"@storybook/addon-notes": "5.1.11",
|
||||
"@storybook/addon-options": "5.1.11",
|
||||
"@storybook/react": "5.1.11",
|
||||
"@testing-library/react-hooks": "2.0.1",
|
||||
"@types/jest": "24.0.18",
|
||||
"@types/react": "16.9.2",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-plugin-dynamic-import-node": "2.3.0",
|
||||
"fork-ts-checker-webpack-plugin": "1.4.3",
|
||||
"gh-pages": "2.1.0",
|
||||
"husky": "3.0.2",
|
||||
"jest": "24.8.0",
|
||||
"fork-ts-checker-webpack-plugin": "1.5.0",
|
||||
"gh-pages": "2.1.1",
|
||||
"husky": "3.0.4",
|
||||
"jest": "24.9.0",
|
||||
"keyboardjs": "2.5.1",
|
||||
"lint-staged": "9.2.1",
|
||||
"lint-staged": "9.2.3",
|
||||
"markdown-loader": "5.1.0",
|
||||
"prettier": "1.17.1",
|
||||
"raf-stub": "^3.0.0",
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6",
|
||||
"prettier": "1.18.2",
|
||||
"raf-stub": "3.0.0",
|
||||
"react": "16.9.0",
|
||||
"react-dom": "16.9.0",
|
||||
"react-spring": "8.0.27",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"react-test-renderer": "16.9.0",
|
||||
"rebound": "0.1.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"rimraf": "2.6.3",
|
||||
"rimraf": "3.0.0",
|
||||
"rxjs": "6.5.2",
|
||||
"semantic-release": "15.13.19",
|
||||
"semantic-release": "15.13.21",
|
||||
"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",
|
||||
|
||||
@ -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>: <span>supported</span> <br />
|
||||
<strong>Battery state</strong>: <span>fetched</span> <br />
|
||||
<strong>Charge level</strong>: <span>{(batteryState.level * 100).toFixed(0)}%</span> <br />
|
||||
<strong>Charging</strong>: <span>{batteryState.charging ? 'yes' : 'no'}</span> <br />
|
||||
<strong>Charging time</strong>:
|
||||
<span>{batteryState.chargingTime ? batteryState.chargingTime : 'finished'}</span> <br />
|
||||
<strong>Discharging time</strong>: <span>{batteryState.dischargingTime}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('Sensors|useBattery', module)
|
||||
|
||||
@ -4,20 +4,33 @@ import { useCounter } from '..';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
const [value, { inc, dec, get, set, reset }] = useCounter(5);
|
||||
const [min, { inc: incMin, dec: decMin }] = useCounter(1);
|
||||
const [max, { inc: incMax, dec: decMax }] = useCounter(10);
|
||||
const [value, { inc, dec, set, reset }] = useCounter(5, max, min);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{value} is {get()}
|
||||
current: {value} [min: {min}; max: {max}]
|
||||
</div>
|
||||
<button onClick={() => inc()}>Increment</button>
|
||||
<br />
|
||||
Current value: <button onClick={() => inc()}>Increment</button>
|
||||
<button onClick={() => dec()}>Decrement</button>
|
||||
<button onClick={() => inc(5)}>Increment (+5)</button>
|
||||
<button onClick={() => dec(5)}>Decrement (-5)</button>
|
||||
<button onClick={() => set(100)}>Set 100</button>
|
||||
<button onClick={() => reset()}>Reset</button>
|
||||
<button onClick={() => reset(25)}>Reset (25)</button>
|
||||
<br />
|
||||
<br />
|
||||
Min value:
|
||||
<button onClick={() => incMin()}>Increment</button>
|
||||
<button onClick={() => decMin()}>Decrement</button>
|
||||
<br />
|
||||
<br />
|
||||
Max value:
|
||||
<button onClick={() => incMax()}>Increment</button>
|
||||
<button onClick={() => decMax()}>Decrement</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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)
|
||||
|
||||
35
src/__stories__/useUpsert.story.tsx
Normal file
35
src/__stories__/useUpsert.story.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
import { useUpsert } from '../index';
|
||||
|
||||
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 />);
|
||||
@ -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', () => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import useCounter from '../useCounter';
|
||||
|
||||
const setUp = (initialValue?: number) => renderHook(() => useCounter(initialValue));
|
||||
const setUp = (initialValue?: number, max: number | null = null, min: number | null = null) =>
|
||||
renderHook(() => useCounter(initialValue, max, min));
|
||||
|
||||
it('should init counter and utils', () => {
|
||||
const { result } = setUp(5);
|
||||
@ -121,8 +122,81 @@ it('should reset and set new original value', () => {
|
||||
expect(get()).toBe(8);
|
||||
});
|
||||
|
||||
it.todo('should log an error if initial value is other than a number');
|
||||
it.todo('should log an error if increment value is other than a number');
|
||||
it.todo('should log an error if increment value is a negative number');
|
||||
it.todo('should log an error if decrement value is other than a number');
|
||||
it.todo('should log an error if decrement value is a negative number');
|
||||
it('should not exceed max value', () => {
|
||||
const { result } = setUp(10, 5);
|
||||
expect(result.current[0]).toBe(5);
|
||||
|
||||
const { get, inc, reset } = result.current[1];
|
||||
|
||||
act(() => reset(10));
|
||||
expect(get()).toBe(5);
|
||||
|
||||
act(() => reset(4));
|
||||
expect(get()).toBe(4);
|
||||
|
||||
act(() => inc());
|
||||
expect(get()).toBe(5);
|
||||
|
||||
act(() => inc());
|
||||
expect(get()).toBe(5);
|
||||
});
|
||||
|
||||
it('should not exceed min value', () => {
|
||||
const { result } = setUp(3, null, 5);
|
||||
expect(result.current[0]).toBe(5);
|
||||
|
||||
const { get, dec, reset } = result.current[1];
|
||||
|
||||
act(() => reset(4));
|
||||
expect(get()).toBe(5);
|
||||
|
||||
act(() => reset(6));
|
||||
expect(get()).toBe(6);
|
||||
|
||||
act(() => dec());
|
||||
expect(get()).toBe(5);
|
||||
|
||||
act(() => dec());
|
||||
expect(get()).toBe(5);
|
||||
});
|
||||
|
||||
describe('should `console.error` on unexpected inputs', () => {
|
||||
it('on any of call parameters', () => {
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// @ts-ignore
|
||||
setUp(false);
|
||||
expect(spy.mock.calls[0][0]).toBe('initialValue has to be a number, got boolean');
|
||||
|
||||
// @ts-ignore
|
||||
setUp(10, false);
|
||||
expect(spy.mock.calls[1][0]).toBe('max has to be a number, got boolean');
|
||||
|
||||
// @ts-ignore
|
||||
setUp(10, 5, {});
|
||||
expect(spy.mock.calls[2][0]).toBe('min has to be a number, got object');
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('on any of returned methods has unexpected input', () => {
|
||||
const { result } = setUp(10);
|
||||
const { inc, dec, reset } = result.current[1];
|
||||
|
||||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// @ts-ignore
|
||||
act(() => inc(false));
|
||||
expect(spy.mock.calls[0][0]).toBe('delta has to be a number, got boolean');
|
||||
|
||||
// @ts-ignore
|
||||
act(() => dec(false));
|
||||
expect(spy.mock.calls[1][0]).toBe('delta has to be a number, got boolean');
|
||||
|
||||
// @ts-ignore
|
||||
act(() => reset({}));
|
||||
expect(spy.mock.calls[2][0]).toBe('value has to be a number, got object');
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
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(5);
|
||||
expect(calls).toBeLessThan(10);
|
||||
|
||||
done();
|
||||
}, 100);
|
||||
it('should be defined', () => {
|
||||
expect(useRafLoop).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return stop function, start function and loop state', () => {
|
||||
@ -29,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();
|
||||
}, 100);
|
||||
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);
|
||||
@ -57,57 +64,39 @@ describe('useRafLoop', () => {
|
||||
act(() => {
|
||||
hook.result.current[0]();
|
||||
});
|
||||
|
||||
expect(hook.result.current[1]).toBe(false);
|
||||
setTimeout(() => {
|
||||
expect(calls).toEqual(0);
|
||||
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
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(5);
|
||||
expect(calls).toBeLessThan(10);
|
||||
|
||||
done();
|
||||
}, 100);
|
||||
}, 100);
|
||||
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();
|
||||
}, 100);
|
||||
expect(spy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
74
src/__tests__/useUpsert.test.ts
Normal file
74
src/__tests__/useUpsert.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -57,6 +57,9 @@ export { default as usePrevious } from './usePrevious';
|
||||
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';
|
||||
@ -76,6 +79,7 @@ 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';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DependencyList, useCallback, useState } from 'react';
|
||||
import useRefMounted from './useRefMounted';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
export type AsyncState<T> =
|
||||
| {
|
||||
@ -30,23 +30,19 @@ export default function useAsyncFn<Result = any, Args extends any[] = any[]>(
|
||||
): AsyncFn<Result, Args> {
|
||||
const [state, set] = useState<AsyncState<Result>>(initialState);
|
||||
|
||||
const mounted = useRefMounted();
|
||||
const isMounted = useMountedState();
|
||||
|
||||
const callback = useCallback((...args: Args | []) => {
|
||||
set({ loading: true });
|
||||
|
||||
return fn(...args).then(
|
||||
value => {
|
||||
if (mounted.current) {
|
||||
set({ value, loading: false });
|
||||
}
|
||||
isMounted() && set({ value, loading: false });
|
||||
|
||||
return value;
|
||||
},
|
||||
error => {
|
||||
if (mounted.current) {
|
||||
set({ error, loading: false });
|
||||
}
|
||||
isMounted() && set({ error, loading: false });
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as writeText from 'copy-to-clipboard';
|
||||
import writeText from 'copy-to-clipboard';
|
||||
import { useCallback } from 'react';
|
||||
import useRefMounted from './useRefMounted';
|
||||
import useMountedState from './useMountedState';
|
||||
import useSetState from './useSetState';
|
||||
|
||||
export interface CopyToClipboardState {
|
||||
@ -10,7 +10,7 @@ export interface CopyToClipboardState {
|
||||
}
|
||||
|
||||
const useCopyToClipboard = (): [CopyToClipboardState, (value: string) => void] => {
|
||||
const mounted = useRefMounted();
|
||||
const isMounted = useMountedState();
|
||||
const [state, setState] = useSetState<CopyToClipboardState>({
|
||||
value: undefined,
|
||||
error: undefined,
|
||||
@ -27,7 +27,7 @@ const useCopyToClipboard = (): [CopyToClipboardState, (value: string) => void] =
|
||||
|
||||
const noUserInteraction = writeText(value);
|
||||
|
||||
if (!mounted.current) {
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
setState({
|
||||
@ -36,7 +36,7 @@ const useCopyToClipboard = (): [CopyToClipboardState, (value: string) => void] =
|
||||
noUserInteraction,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted.current) {
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
setState({
|
||||
|
||||
@ -9,14 +9,70 @@ export interface CounterActions {
|
||||
reset: (value?: number) => void;
|
||||
}
|
||||
|
||||
const useCounter = (initialValue: number = 0): [number, CounterActions] => {
|
||||
const [get, set] = useGetSet<number>(initialValue);
|
||||
const inc = useCallback((delta: number = 1) => set(get() + delta), []);
|
||||
const dec = useCallback((delta: number = 1) => inc(-delta), []);
|
||||
const reset = useCallback((value: number = initialValue) => {
|
||||
initialValue = value;
|
||||
set(value);
|
||||
}, []);
|
||||
export default function useCounter(
|
||||
initialValue: number = 0,
|
||||
max: number | null = null,
|
||||
min: number | null = null
|
||||
): [number, CounterActions] {
|
||||
typeof initialValue !== 'number' && console.error('initialValue has to be a number, got ' + typeof initialValue);
|
||||
|
||||
if (typeof min === 'number') {
|
||||
initialValue = Math.max(initialValue, min);
|
||||
} else if (min !== null) {
|
||||
console.error('min has to be a number, got ' + typeof min);
|
||||
}
|
||||
|
||||
if (typeof max === 'number') {
|
||||
initialValue = Math.min(initialValue, max);
|
||||
} else if (max !== null) {
|
||||
console.error('max has to be a number, got ' + typeof max);
|
||||
}
|
||||
|
||||
const [get, setInternal] = useGetSet<number>(initialValue);
|
||||
|
||||
function set(value: number): void {
|
||||
const current = get();
|
||||
|
||||
if (current === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof min === 'number') {
|
||||
value = Math.max(value, min);
|
||||
}
|
||||
if (typeof max === 'number') {
|
||||
value = Math.min(value, max);
|
||||
}
|
||||
|
||||
current !== value && setInternal(value);
|
||||
}
|
||||
|
||||
const inc = useCallback(
|
||||
(delta: number = 1) => {
|
||||
typeof delta !== 'number' && console.error('delta has to be a number, got ' + typeof delta);
|
||||
|
||||
set(get() + delta);
|
||||
},
|
||||
[max, min]
|
||||
);
|
||||
const dec = useCallback(
|
||||
(delta: number = 1) => {
|
||||
typeof delta !== 'number' && console.error('delta has to be a number, got ' + typeof delta);
|
||||
|
||||
set(get() - delta);
|
||||
},
|
||||
[max, min]
|
||||
);
|
||||
const reset = useCallback(
|
||||
(value: number = initialValue) => {
|
||||
typeof value !== 'number' && console.error('value has to be a number, got ' + typeof value);
|
||||
|
||||
initialValue = value;
|
||||
set(value);
|
||||
},
|
||||
[max, min]
|
||||
);
|
||||
|
||||
const actions = {
|
||||
inc,
|
||||
dec,
|
||||
@ -26,6 +82,4 @@ const useCounter = (initialValue: number = 0): [number, CounterActions] => {
|
||||
};
|
||||
|
||||
return [get(), actions];
|
||||
};
|
||||
|
||||
export default useCounter;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
|
||||
import * as isEqual from 'react-fast-compare';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
const isPrimitive = (val: any) => val !== Object(val);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import useRefMounted from './useRefMounted';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
const { useState, useMemo, useCallback, useEffect } = React;
|
||||
|
||||
@ -22,16 +22,13 @@ export interface DropAreaOptions {
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
/*
|
||||
/*
|
||||
const defaultState: DropAreaState = {
|
||||
over: false,
|
||||
};
|
||||
};
|
||||
*/
|
||||
|
||||
const createProcess = (options: DropAreaOptions, mounted: React.RefObject<boolean>) => (
|
||||
dataTransfer: DataTransfer,
|
||||
event
|
||||
) => {
|
||||
const createProcess = (options: DropAreaOptions, mounted: boolean) => (dataTransfer: DataTransfer, event) => {
|
||||
const uri = dataTransfer.getData('text/uri-list');
|
||||
|
||||
if (uri) {
|
||||
@ -46,7 +43,7 @@ const createProcess = (options: DropAreaOptions, mounted: React.RefObject<boolea
|
||||
|
||||
if (dataTransfer.items && dataTransfer.items.length) {
|
||||
dataTransfer.items[0].getAsString(text => {
|
||||
if (mounted.current) {
|
||||
if (mounted) {
|
||||
(options.onText || noop)(text, event);
|
||||
}
|
||||
});
|
||||
@ -55,10 +52,10 @@ const createProcess = (options: DropAreaOptions, mounted: React.RefObject<boolea
|
||||
|
||||
const useDrop = (options: DropAreaOptions = {}, args = []): DropAreaState => {
|
||||
const { onFiles, onText, onUri } = options;
|
||||
const mounted = useRefMounted();
|
||||
const isMounted = useMountedState();
|
||||
const [over, setOverRaw] = useState<boolean>(false);
|
||||
const setOver = useCallback(setOverRaw, []);
|
||||
const process = useMemo(() => createProcess(options, mounted), [onFiles, onText, onUri]);
|
||||
const process = useMemo(() => createProcess(options, isMounted()), [onFiles, onText, onUri]);
|
||||
|
||||
useEffect(() => {
|
||||
const onDragOver = event => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import useRefMounted from './useRefMounted';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
export interface DropAreaState {
|
||||
over: boolean;
|
||||
@ -20,16 +20,13 @@ export interface DropAreaOptions {
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
/*
|
||||
/*
|
||||
const defaultState: DropAreaState = {
|
||||
over: false,
|
||||
};
|
||||
};
|
||||
*/
|
||||
|
||||
const createProcess = (options: DropAreaOptions, mounted: React.RefObject<boolean>) => (
|
||||
dataTransfer: DataTransfer,
|
||||
event
|
||||
) => {
|
||||
const createProcess = (options: DropAreaOptions, mounted: boolean) => (dataTransfer: DataTransfer, event) => {
|
||||
const uri = dataTransfer.getData('text/uri-list');
|
||||
|
||||
if (uri) {
|
||||
@ -44,7 +41,7 @@ const createProcess = (options: DropAreaOptions, mounted: React.RefObject<boolea
|
||||
|
||||
if (dataTransfer.items && dataTransfer.items.length) {
|
||||
dataTransfer.items[0].getAsString(text => {
|
||||
if (mounted.current) {
|
||||
if (mounted) {
|
||||
(options.onText || noop)(text, event);
|
||||
}
|
||||
});
|
||||
@ -76,9 +73,9 @@ const createBond = (process, setOver): DropAreaBond => ({
|
||||
|
||||
const useDropArea = (options: DropAreaOptions = {}): [DropAreaBond, DropAreaState] => {
|
||||
const { onFiles, onText, onUri } = options;
|
||||
const mounted = useRefMounted();
|
||||
const isMounted = useMountedState();
|
||||
const [over, setOver] = useState<boolean>(false);
|
||||
const process = useMemo(() => createProcess(options, mounted), [onFiles, onText, onUri]);
|
||||
const process = useMemo(() => createProcess(options, isMounted()), [onFiles, onText, onUri]);
|
||||
const bond: DropAreaBond = useMemo(() => createBond(process, setOver), [process, setOver]);
|
||||
|
||||
return [bond, { over }];
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
export default function useMountedState(): () => boolean {
|
||||
const mountedRef = useRef<boolean>(false);
|
||||
const get = useCallback(() => mountedRef.current, []);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
@ -11,5 +12,5 @@ export default function useMountedState(): () => boolean {
|
||||
};
|
||||
});
|
||||
|
||||
return () => mountedRef.current;
|
||||
return get;
|
||||
}
|
||||
|
||||
@ -37,8 +37,8 @@ const useMouse = (ref: RefObject<Element>): State => {
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
if (ref && ref.current) {
|
||||
const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
|
||||
const posX = left + window.scrollX;
|
||||
const posY = top + window.scrollY;
|
||||
const posX = left + window.pageXOffset;
|
||||
const posY = top + window.pageYOffset;
|
||||
const elX = event.pageX - posX;
|
||||
const elY = event.pageY - posY;
|
||||
|
||||
|
||||
@ -1,22 +1,18 @@
|
||||
import { useCallback } from 'react';
|
||||
import useRefMounted from './useRefMounted';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
export type UsePromise = () => <T>(promise: Promise<T>) => Promise<T>;
|
||||
|
||||
const usePromise: UsePromise = () => {
|
||||
const refMounted = useRefMounted();
|
||||
const isMounted = useMountedState();
|
||||
return useCallback(
|
||||
(promise: Promise<any>) =>
|
||||
new Promise<any>((resolve, reject) => {
|
||||
const onValue = value => {
|
||||
if (refMounted.current) {
|
||||
resolve(value);
|
||||
}
|
||||
isMounted() && resolve(value);
|
||||
};
|
||||
const onError = error => {
|
||||
if (refMounted.current) {
|
||||
reject(error);
|
||||
}
|
||||
isMounted() && reject(error);
|
||||
};
|
||||
promise.then(onValue, onError);
|
||||
}),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -3,14 +3,13 @@ import { useEffect, useRef } from 'react';
|
||||
const useUpdateEffect: typeof useEffect = (effect, deps) => {
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
useEffect(
|
||||
isInitialMount.current
|
||||
? () => {
|
||||
isInitialMount.current = false;
|
||||
}
|
||||
: effect,
|
||||
deps
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
} else {
|
||||
return effect();
|
||||
}
|
||||
}, deps);
|
||||
};
|
||||
|
||||
export default useUpdateEffect;
|
||||
|
||||
39
src/useUpsert.ts
Normal file
39
src/useUpsert.ts
Normal 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;
|
||||
@ -9,6 +9,7 @@
|
||||
"rootDir": "src",
|
||||
"sourceMap": false,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user