mirror of
https://github.com/streamich/react-use.git
synced 2026-01-25 14:17:16 +00:00
Merge remote-tracking branch 'origin/master' into pr/857
This commit is contained in:
commit
d0830b62e1
@ -3,7 +3,7 @@ version: 2
|
||||
refs:
|
||||
container: &container
|
||||
docker:
|
||||
- image: node:12.14.0
|
||||
- image: node:12.15.0
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- &Versions
|
||||
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "react-app"
|
||||
}
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
169
CHANGELOG.md
169
CHANGELOG.md
@ -1,3 +1,172 @@
|
||||
# [13.25.0](https://github.com/streamich/react-use/compare/v13.24.1...v13.25.0) (2020-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **useBeforeUnload:** allow passing a dirty function ([#842](https://github.com/streamich/react-use/issues/842)) ([c4a14a4](https://github.com/streamich/react-use/commit/c4a14a4fb370c7628e4cc5861e31cc64a66b64b0))
|
||||
|
||||
## [13.24.1](https://github.com/streamich/react-use/compare/v13.24.0...v13.24.1) (2020-02-15)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* use fast-deep-equal for deep comparisons ([b9a8aad](https://github.com/streamich/react-use/commit/b9a8aad053a40028f119192ddecedb5c7ec05247))
|
||||
|
||||
# [13.24.0](https://github.com/streamich/react-use/compare/v13.23.0...v13.24.0) (2020-02-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add createReducerContext and createStateContext factories ([84b8310](https://github.com/streamich/react-use/commit/84b83101c2253f8935b2804a48ade081e41982a8))
|
||||
|
||||
# [13.23.0](https://github.com/streamich/react-use/compare/v13.22.5...v13.23.0) (2020-02-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add createGlobalState hook generator ([fda7199](https://github.com/streamich/react-use/commit/fda7199b7da23f321e68d0784deb1f0f3d273e3c))
|
||||
|
||||
## [13.22.5](https://github.com/streamich/react-use/compare/v13.22.4...v13.22.5) (2020-02-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 don't throw in useMediaDevices on missing browser API ([0f119fe](https://github.com/streamich/react-use/commit/0f119fe23e837e0d8c2a8c882b1aaf3b62cbc7d2))
|
||||
* handle undefined mediaDevices ([6f68437](https://github.com/streamich/react-use/commit/6f68437359704dace7d518f1f013bc3516400c67))
|
||||
|
||||
## [13.22.4](https://github.com/streamich/react-use/compare/v13.22.3...v13.22.4) (2020-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @xobotyi/scrollbar-width to v1.8.2 ([#930](https://github.com/streamich/react-use/issues/930)) ([727b950](https://github.com/streamich/react-use/commit/727b95096ec6654ba4da22f6825e6d8982258033))
|
||||
|
||||
## [13.22.3](https://github.com/streamich/react-use/compare/v13.22.2...v13.22.3) (2020-01-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **useIntersection:** disconnect an old IntersectionObserver instance when the ref changes ([ac2f54a](https://github.com/streamich/react-use/commit/ac2f54a8f683296feecfeeb6354738b9ebbc36d0))
|
||||
* **useIntersection:** reset an intersectionObserverEntry when the ref changes ([3f8687e](https://github.com/streamich/react-use/commit/3f8687e1f51cc48efbf6be3f0677f5bd06ecba08))
|
||||
* **useIntersection:** return null if IntersectionObserver is not supported ([4f6d388](https://github.com/streamich/react-use/commit/4f6d3887be5cf62ce42357a7bf27f4ae8b080eba))
|
||||
|
||||
## [13.22.2](https://github.com/streamich/react-use/compare/v13.22.1...v13.22.2) (2020-01-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @xobotyi/scrollbar-width to v1.7.0 ([db74101](https://github.com/streamich/react-use/commit/db741019324c3d20a17bbc20a014cedd21e66b1a))
|
||||
|
||||
## [13.22.1](https://github.com/streamich/react-use/compare/v13.22.0...v13.22.1) (2020-01-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @xobotyi/scrollbar-width to v1.6.0 ([431ba5d](https://github.com/streamich/react-use/commit/431ba5d0816cb7701b03460c5efa5199ad27cbc4))
|
||||
|
||||
# [13.22.0](https://github.com/streamich/react-use/compare/v13.21.0...v13.22.0) (2020-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fail testing and update doc ([57b9041](https://github.com/streamich/react-use/commit/57b904118e2cd1f1b4e367c9a14e2a981db2f06a))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add useLongPress hook ([45681b8](https://github.com/streamich/react-use/commit/45681b88e3fd3d9337a38da07248c46ec6d5956e))
|
||||
|
||||
# [13.21.0](https://github.com/streamich/react-use/compare/v13.20.0...v13.21.0) (2020-01-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Typings for `useDefault` ([fa0f53b](https://github.com/streamich/react-use/commit/fa0f53bf86a712f4b7e503cdf4718a8ff5448e05))
|
||||
|
||||
# [13.20.0](https://github.com/streamich/react-use/compare/v13.19.0...v13.20.0) (2020-01-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* `useMap`: allow resetting with provided value other then initial ([7645f72](https://github.com/streamich/react-use/commit/7645f7249dbf52db140dfc8b7866cb4a171e439c))
|
||||
|
||||
# [13.19.0](https://github.com/streamich/react-use/compare/v13.18.0...v13.19.0) (2020-01-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add useError hook ([65f3644](https://github.com/streamich/react-use/commit/65f364420524bacebe8f8149b8197fb62bff1a08))
|
||||
|
||||
# [13.18.0](https://github.com/streamich/react-use/compare/v13.17.0...v13.18.0) (2020-01-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* check for null ([d619c39](https://github.com/streamich/react-use/commit/d619c39a21e9f0b4b4bfc6a209311bf0bd495f9b))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add serializer/deserializer option to useLocalStorage ([5316510](https://github.com/streamich/react-use/commit/5316510babf7606a2f4b78de2b0eb85c930890cf))
|
||||
|
||||
# [13.17.0](https://github.com/streamich/react-use/compare/v13.16.1...v13.17.0) (2020-01-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for body lock on iOS ([d778408](https://github.com/streamich/react-use/commit/d7784084fe84aca72efe85260101b00ef1df7580))
|
||||
|
||||
## [13.16.1](https://github.com/streamich/react-use/compare/v13.16.0...v13.16.1) (2020-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update the types dep for js-cookie ([5c55d59](https://github.com/streamich/react-use/commit/5c55d59a7d1d799cba7af87e15ab4a4b27a8fc67))
|
||||
|
||||
# [13.16.0](https://github.com/streamich/react-use/compare/v13.15.0...v13.16.0) (2020-01-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add option to useTitle to restore title on un-mount ([b8b3e47](https://github.com/streamich/react-use/commit/b8b3e479cea6071d4310bac29f138bd8917eee0b))
|
||||
|
||||
# [13.15.0](https://github.com/streamich/react-use/compare/v13.14.3...v13.15.0) (2020-01-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add useCookie hook ([4e5c90f](https://github.com/streamich/react-use/commit/4e5c90f021f56ae2008dc25daad69c43063f608f))
|
||||
|
||||
## [13.14.3](https://github.com/streamich/react-use/compare/v13.14.2...v13.14.3) (2020-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* useUpdateEffect cleanup test returns false positive ([9b31c42](https://github.com/streamich/react-use/commit/9b31c42ccb42fe13fc24f7434b00a1bcbee8cd8a))
|
||||
* useUpdateEffect test returning false positive ([#865](https://github.com/streamich/react-use/issues/865)) ([1946006](https://github.com/streamich/react-use/commit/1946006c4224bc61eabb797f4cdd7d20fff7ab25))
|
||||
|
||||
## [13.14.2](https://github.com/streamich/react-use/compare/v13.14.1...v13.14.2) (2020-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump fast-shallow-equal ([19b2b39](https://github.com/streamich/react-use/commit/19b2b39eeae3898bd8d8e3478eb7459372bb09d5))
|
||||
|
||||
## [13.14.1](https://github.com/streamich/react-use/compare/v13.14.0...v13.14.1) (2020-01-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* useUpdateEffect returns optional cleanup function ([0ce421c](https://github.com/streamich/react-use/commit/0ce421ced78fd6eb06a5676fefb856e18bfcacc1))
|
||||
* useUpdateEffect returns optional cleanup function ([#864](https://github.com/streamich/react-use/issues/864)) ([7960127](https://github.com/streamich/react-use/commit/7960127a98c0d3c33000088fbd97804d41084f7d))
|
||||
|
||||
# [13.14.0](https://github.com/streamich/react-use/compare/v13.13.0...v13.14.0) (2020-01-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 add [vertical] flag to useSlider() hook ([777865c](https://github.com/streamich/react-use/commit/777865c3ac6772fbda2bc0a6f58cde3eff7dec43))
|
||||
|
||||
# [13.13.0](https://github.com/streamich/react-use/compare/v13.12.2...v13.13.0) (2019-12-27)
|
||||
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
- [`useIntersection`](./docs/useIntersection.md) — tracks an HTML element's intersection. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-useintersection--demo)
|
||||
- [`useKey`](./docs/useKey.md), [`useKeyPress`](./docs/useKeyPress.md), [`useKeyboardJs`](./docs/useKeyboardJs.md), and [`useKeyPressEvent`](./docs/useKeyPressEvent.md) — track keys. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usekeypressevent--demo)
|
||||
- [`useLocation`](./docs/useLocation.md) and [`useSearchParam`](./docs/useSearchParam.md) — tracks page navigation bar location state.
|
||||
- [`useLongPress`](./docs/useLongPress.md) — tracks long press gesture of some element.
|
||||
- [`useMedia`](./docs/useMedia.md) — tracks state of a CSS media query. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemedia--demo)
|
||||
- [`useMediaDevices`](./docs/useMediaDevices.md) — tracks state of connected hardware devices.
|
||||
- [`useMotion`](./docs/useMotion.md) — tracks state of device's motion sensor.
|
||||
@ -75,6 +76,7 @@
|
||||
- [`useCss`](./docs/useCss.md) — dynamically adjusts CSS.
|
||||
- [`useDrop` and `useDropArea`](./docs/useDrop.md) — tracks file, link and copy-paste drops.
|
||||
- [`useFullscreen`](./docs/useFullscreen.md) — display an element or video full-screen. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-usefullscreen--demo)
|
||||
- [`useSlider`](./docs/useSlider.md) — provides slide behavior over any HTML element. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-useslider--demo)
|
||||
- [`useSpeech`](./docs/useSpeech.md) — synthesizes speech from a text string. [![][img-demo]](https://codesandbox.io/s/n090mqz69m)
|
||||
- [`useVibrate`](./docs/useVibrate.md) — provide physical feedback using the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API). [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-usevibrate--demo)
|
||||
- [`useVideo`](./docs/useVideo.md) — plays video, tracks its state, and exposes playback controls. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-usevideo--demo)
|
||||
@ -93,8 +95,10 @@
|
||||
- [**Side-effects**](./docs/Side-effects.md)
|
||||
- [`useAsync`](./docs/useAsync.md), [`useAsyncFn`](./docs/useAsyncFn.md), and [`useAsyncRetry`](./docs/useAsyncRetry.md) — resolves an `async` function.
|
||||
- [`useBeforeUnload`](./docs/useBeforeUnload.md) — shows browser alert when user try to reload or close the page.
|
||||
- [`useCookie`](./docs/useCookie.md) — provides way to read, update and delete a cookie. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usecookie--demo)
|
||||
- [`useCopyToClipboard`](./docs/useCopyToClipboard.md) — copies text to clipboard.
|
||||
- [`useDebounce`](./docs/useDebounce.md) — debounces a function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usedebounce--demo)
|
||||
- [`useError`](./docs/useError.md) — error dispatcher. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-useerror--demo)
|
||||
- [`useFavicon`](./docs/useFavicon.md) — sets favicon of the page.
|
||||
- [`useLocalStorage`](./docs/useLocalStorage.md) — manages a value in `localStorage`.
|
||||
- [`useLockBodyScroll`](./docs/useLockBodyScroll.md) — lock scrolling of the body element.
|
||||
@ -122,6 +126,7 @@
|
||||
- [**State**](./docs/State.md)
|
||||
- [`createMemo`](./docs/createMemo.md) — factory of memoized hooks.
|
||||
- [`createReducer`](./docs/createReducer.md) — factory of reducer hooks with custom middleware.
|
||||
- [`createReducerContext`](./docs/createReducerContext.md) and [`createStateContext`](./docs/createStateContext.md) — factory of hooks for a sharing state between components.
|
||||
- [`useDefault`](./docs/useDefault.md) — returns the default value when state is `null` or `undefined`.
|
||||
- [`useGetSet`](./docs/useGetSet.md) — returns state getter `get()` instead of raw state.
|
||||
- [`useGetSetState`](./docs/useGetSetState.md) — as if [`useGetSet`](./docs/useGetSet.md) and [`useSetState`](./docs/useSetState.md) had a baby.
|
||||
@ -143,12 +148,12 @@
|
||||
- [`useMediatedState`](./docs/useMediatedState.md) — like the regular `useState` but with mediation by custom function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemediatedstate--demo)
|
||||
- [`useFirstMountState`](./docs/useFirstMountState.md) — check if current render is first. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usefirstmountstate--demo)
|
||||
- [`useRendersCount`](./docs/useRendersCount.md) — count component renders. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userenderscount--demo)
|
||||
- [`createGlobalState`](./docs/createGlobalState.md) — cross component shared state.[![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-createglobalstate--demo)
|
||||
<br/>
|
||||
<br/>
|
||||
- [**Miscellaneous**]()
|
||||
- [`useEnsuredForwardedRef`](./docs/useEnsuredForwardedRef.md) and [`ensuredForwardRef`](./docs/useEnsuredForwardedRef.md) — use a React.forwardedRef safely. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-useensuredforwardedref--demo)
|
||||
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
32
docs/createGlobalState.md
Normal file
32
docs/createGlobalState.md
Normal file
@ -0,0 +1,32 @@
|
||||
# `useGlobalState`
|
||||
|
||||
A React hook which creates a globally shared state.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
const useGlobalValue = createGlobalState<number>(0);
|
||||
|
||||
const CompA: FC = () => {
|
||||
const [value, setValue] = useGlobalValue();
|
||||
|
||||
return <button onClick={() => setValue(value + 1)}>+</button>;
|
||||
};
|
||||
|
||||
const CompB: FC = () => {
|
||||
const [value, setValue] = useGlobalValue();
|
||||
|
||||
return <button onClick={() => setValue(value - 1)}>-</button>;
|
||||
};
|
||||
|
||||
const Demo: FC = () => {
|
||||
const [value] = useGlobalValue();
|
||||
return (
|
||||
<div>
|
||||
<p>{value}</p>
|
||||
<CompA />
|
||||
<CompB />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
91
docs/createReducerContext.md
Normal file
91
docs/createReducerContext.md
Normal file
@ -0,0 +1,91 @@
|
||||
# `createReducerContext`
|
||||
|
||||
Factory for react context hooks that will behave just like [React's `useReducer`](https://reactjs.org/docs/hooks-reference.html#usereducer) except the state will be shared among all components in the provider.
|
||||
|
||||
This allows you to have a shared state that any component can update easily.
|
||||
|
||||
## Usage
|
||||
|
||||
An example with two counters that shared the same value.
|
||||
|
||||
```jsx
|
||||
import { createReducerContext } from 'react-use';
|
||||
|
||||
type Action = 'increment' | 'decrement';
|
||||
|
||||
const reducer = (state: number, action: Action) => {
|
||||
switch (action) {
|
||||
case 'increment':
|
||||
return state + 1;
|
||||
case 'decrement':
|
||||
return state - 1;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
const [useSharedCounter, SharedCounterProvider] = createReducerContext(reducer, 0);
|
||||
|
||||
const ComponentA = () => {
|
||||
const [count, dispatch] = useSharedCounter();
|
||||
return (
|
||||
<p>
|
||||
Component A
|
||||
<button type="button" onClick={() => dispatch('decrement')}>
|
||||
-
|
||||
</button>
|
||||
{count}
|
||||
<button type="button" onClick={() => dispatch('increment')}>
|
||||
+
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const ComponentB = () => {
|
||||
const [count, dispatch] = useSharedCounter();
|
||||
return (
|
||||
<p>
|
||||
Component B
|
||||
<button type="button" onClick={() => dispatch('decrement')}>
|
||||
-
|
||||
</button>
|
||||
{count}
|
||||
<button type="button" onClick={() => dispatch('increment')}>
|
||||
+
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<SharedCounterProvider>
|
||||
<p>Those two counters share the same value.</p>
|
||||
<ComponentA />
|
||||
<ComponentB />
|
||||
</SharedCounterProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
```jsx
|
||||
const [useSharedState, SharedStateProvider] = createReducerContext(reducer, initialState);
|
||||
|
||||
// In wrapper
|
||||
const Wrapper = ({ children }) => (
|
||||
// You can override the initial state for each Provider
|
||||
<SharedStateProvider initialState={overrideInitialState}>
|
||||
{ children }
|
||||
</SharedStateProvider>
|
||||
)
|
||||
|
||||
// In a component
|
||||
const Component = () => {
|
||||
const [sharedState, dispatch] = useSharedState();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
68
docs/createStateContext.md
Normal file
68
docs/createStateContext.md
Normal file
@ -0,0 +1,68 @@
|
||||
# `createStateContext`
|
||||
|
||||
Factory for react context hooks that will behave just like [React's `useState`](https://reactjs.org/docs/hooks-reference.html#usestate) except the state will be shared among all components in the provider.
|
||||
|
||||
This allows you to have a shared state that any component can update easily.
|
||||
|
||||
## Usage
|
||||
|
||||
An example with a shared text between two input fields.
|
||||
|
||||
```jsx
|
||||
import { createStateContext } from 'react-use';
|
||||
|
||||
const [useSharedText, SharedTextProvider] = createStateContext('');
|
||||
|
||||
const ComponentA = () => {
|
||||
const [text, setText] = useSharedText();
|
||||
return (
|
||||
<p>
|
||||
Component A:
|
||||
<br />
|
||||
<input type="text" value={text} onInput={ev => setText(ev.target.value)} />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const ComponentB = () => {
|
||||
const [text, setText] = useSharedText();
|
||||
return (
|
||||
<p>
|
||||
Component B:
|
||||
<br />
|
||||
<input type="text" value={text} onInput={ev => setText(ev.target.value)} />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<SharedTextProvider>
|
||||
<p>Those two fields share the same value.</p>
|
||||
<ComponentA />
|
||||
<ComponentB />
|
||||
</SharedTextProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
```jsx
|
||||
const [useSharedState, SharedStateProvider] = createStateContext(initialValue);
|
||||
|
||||
// In wrapper
|
||||
const Wrapper = ({ children }) => (
|
||||
// You can override the initial value for each Provider
|
||||
<SharedStateProvider initialValue={overrideInitialValue}>
|
||||
{ children }
|
||||
</SharedStateProvider>
|
||||
)
|
||||
|
||||
// In a component
|
||||
const Component = () => {
|
||||
const [sharedState, setSharedState] = useSharedState();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@ -5,6 +5,8 @@ React side-effect hook that shows browser alert when user try to reload or close
|
||||
|
||||
## Usage
|
||||
|
||||
### Boolean check
|
||||
|
||||
```jsx
|
||||
import {useBeforeUnload} from 'react-use';
|
||||
|
||||
@ -20,3 +22,28 @@ const Demo = () => {
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Function check
|
||||
|
||||
Note: Since every `dirtyFn` change registers a new callback, you should use
|
||||
[refs](https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback)
|
||||
if your test value changes often.
|
||||
|
||||
```jsx
|
||||
import {useBeforeUnload} from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const [dirty, toggleDirty] = useToggle(false);
|
||||
const dirtyFn = useCallback(() => {
|
||||
return dirty;
|
||||
}, [dirty]);
|
||||
useBeforeUnload(dirtyFn, 'You have unsaved changes, are you sure?');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{dirty && <p>Try to reload or close tab</p>}
|
||||
<button onClick={() => toggleDirty()}>{dirty ? 'Disable' : 'Enable'}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
39
docs/useCookie.md
Normal file
39
docs/useCookie.md
Normal file
@ -0,0 +1,39 @@
|
||||
# `useCookie`
|
||||
|
||||
React hook that returns the current value of a `cookie`, a callback to update the `cookie`
|
||||
and a callback to delete the `cookie.`
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import { useCookie } from "react-use";
|
||||
|
||||
const Demo = () => {
|
||||
const [value, updateCookie, deleteCookie] = useCookie("my-cookie");
|
||||
const [counter, setCounter] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
deleteCookie();
|
||||
}, []);
|
||||
|
||||
const updateCookieHandler = () => {
|
||||
updateCookie(`my-awesome-cookie-${counter}`);
|
||||
setCounter(c => c + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Value: {value}</p>
|
||||
<button onClick={updateCookieHandler}>Update Cookie</button>
|
||||
<br />
|
||||
<button onClick={deleteCookie}>Delete Cookie</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
```ts
|
||||
const [value, updateCookie, deleteCookie] = useCookie(cookieName: string);
|
||||
```
|
||||
@ -5,7 +5,7 @@ A modified useEffect hook that accepts a comparator which is used for comparison
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import {useCounter, useDeepCompareEffect} from 'react-use';
|
||||
import {useCounter, useCustomCompareEffect} from 'react-use';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
const Demo = () => {
|
||||
|
||||
34
docs/useError.md
Normal file
34
docs/useError.md
Normal file
@ -0,0 +1,34 @@
|
||||
# `useError`
|
||||
|
||||
React side-effect hook that returns an error dispatcher.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import { useError } from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const dispatchError = useError();
|
||||
|
||||
const clickHandler = () => {
|
||||
dispatchError(new Error('Some error!'));
|
||||
};
|
||||
|
||||
return <button onClick={clickHandler}>Click me to throw</button>;
|
||||
};
|
||||
|
||||
// In parent app
|
||||
const App = () => (
|
||||
<ErrorBoundary>
|
||||
<Demo />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
```js
|
||||
const dispatchError = useError();
|
||||
```
|
||||
|
||||
- `dispatchError` — Callback of type `(err: Error) => void`
|
||||
@ -2,11 +2,10 @@
|
||||
|
||||
React side-effect hook that manages a single `localStorage` key.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import {useLocalStorage} from 'react-use';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const [value, setValue] = useLocalStorage('my-key', 'foo');
|
||||
@ -21,15 +20,21 @@ const Demo = () => {
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
```js
|
||||
useLocalStorage(key);
|
||||
useLocalStorage(key, initialValue);
|
||||
useLocalStorage(key, initialValue, raw);
|
||||
useLocalStorage(key, initialValue, { raw: true });
|
||||
useLocalStorage(key, initialValue, {
|
||||
raw: false,
|
||||
serializer: (value: T) => string,
|
||||
deserializer: (value: string) => T,
|
||||
});
|
||||
```
|
||||
|
||||
- `key` — `localStorage` key to manage.
|
||||
- `initialValue` — initial value to set, if value in `localStorage` is empty.
|
||||
- `raw` — boolean, if set to `true`, hook will not attempt to JSON serialize stored values.
|
||||
- `serializer` — custom serializer (defaults to `JSON.stringify`)
|
||||
- `deserializer` — custom deserializer (defaults to `JSON.parse`)
|
||||
|
||||
46
docs/useLongPress.md
Normal file
46
docs/useLongPress.md
Normal file
@ -0,0 +1,46 @@
|
||||
# `useLongPress`
|
||||
|
||||
React sensor hook that fires a callback after long pressing.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import { useLongPress } from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const onLongPress = () => {
|
||||
console.log('calls callback after long pressing 300ms');
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
isPreventDefault: true,
|
||||
delay: 300,
|
||||
};
|
||||
const longPressEvent = useLongPress(onLongPress, defaultOptions);
|
||||
|
||||
return <button {...longPressEvent}>useLongPress</button>;
|
||||
};
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
```ts
|
||||
const {
|
||||
onMouseDown,
|
||||
onTouchStart,
|
||||
onMouseUp,
|
||||
onMouseLeave,
|
||||
onTouchEnd
|
||||
} = useLongPress(
|
||||
callback: (e: TouchEvent | MouseEvent) => void,
|
||||
options?: {
|
||||
isPreventDefault?: true,
|
||||
delay?: 300
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
- `callback` — callback function.
|
||||
- `options?` — optional parameter.
|
||||
- `isPreventDefault?` — whether to call `event.preventDefault()` of `touchend` event, for preventing ghost click on mobile devices in some cases, defaults to `true`.
|
||||
- `delay?` — delay in milliseconds after which to calls provided callback, defaults to `300`.
|
||||
@ -8,7 +8,7 @@ React state hook that tracks a value of an object.
|
||||
import {useMap} from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const [map, {set, remove, reset}] = useMap({
|
||||
const [map, {set, setAll, remove, reset}] = useMap({
|
||||
hello: 'there',
|
||||
});
|
||||
|
||||
@ -20,6 +20,9 @@ const Demo = () => {
|
||||
<button onClick={() => reset()}>
|
||||
Reset
|
||||
</button>
|
||||
<button onClick={() => setAll({ hello: 'new', data: 'data' })}>
|
||||
Set new data
|
||||
</button>
|
||||
<button onClick={() => remove('hello')} disabled={!map.hello}>
|
||||
Remove 'hello'
|
||||
</button>
|
||||
|
||||
25
docs/useSlider.md
Normal file
25
docs/useSlider.md
Normal file
@ -0,0 +1,25 @@
|
||||
# `useSlider`
|
||||
|
||||
React UI hook that provides slide behavior over any HTML element. Supports both mouse and touch events.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import {useSlider} from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const ref = React.useRef(null);
|
||||
const {isSliding, value, pos, length} = useSlider(ref);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={ref} style={{ position: 'relative' }}>
|
||||
<p style={{ textAlign: 'center', color: isSliding ? 'red' : 'green' }}>
|
||||
{Math.round(state.value * 100)}%
|
||||
</p>
|
||||
<div style={{ position: 'absolute', left: pos }}>🎚</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
@ -46,7 +46,7 @@ If `stateSet` changed, became shorter than before and `currentIndex` left in shr
|
||||
- **`state`**_`: T`_ — current state value;
|
||||
- **`currentIndex`**_`: number`_ — current state index;
|
||||
- **`prev()`**_`: void`_ — switches state to the previous one. If first element selected it will switch to the last one;
|
||||
- **`nexct()`**_`: void`_ — switches state to the next one. If last element selected it will switch to the first one;
|
||||
- **`next()`**_`: void`_ — switches state to the next one. If last element selected it will switch to the first one;
|
||||
- **`setStateAt(newIndex: number)`**_`: void`_ — set the arbitrary state by index. Indexes are looped, and can be negative.
|
||||
_4ex:_ if list contains 5 elements, attempt to set index 9 will bring use to the 5th element, in case of negative index it will start counting from the right, so -17 will bring us to the 4th element.
|
||||
- **`setState(state: T)`**_`: void`_ — set the arbitrary state value that exists in `stateSet`. _In case new state does not exists in `stateSet` an Error will be thrown._
|
||||
|
||||
88
package.json
88
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-use",
|
||||
"version": "13.13.0",
|
||||
"version": "13.25.0",
|
||||
"description": "Collection of React Hooks",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -16,7 +16,7 @@
|
||||
"test": "jest --maxWorkers 2",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "tslint '{src,tests}/**/*.{ts,tsx}' -t verbose",
|
||||
"lint": "eslint '{src,tests}/**/*.{ts,tsx}'",
|
||||
"lint:fix": "yarn lint --fix",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"build:cjs": "tsc",
|
||||
@ -46,11 +46,13 @@
|
||||
},
|
||||
"homepage": "https://github.com/streamich/react-use#readme",
|
||||
"dependencies": {
|
||||
"@xobotyi/scrollbar-width": "1.5.0",
|
||||
"@types/js-cookie": "2.2.4",
|
||||
"@xobotyi/scrollbar-width": "1.8.2",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"fast-shallow-equal": "^0.1.1",
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-shallow-equal": "^1.0.0",
|
||||
"js-cookie": "^2.2.1",
|
||||
"nano-css": "^5.2.1",
|
||||
"react-fast-compare": "^2.0.4",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"screenfull": "^5.0.0",
|
||||
"set-harmonic-interval": "^1.0.1",
|
||||
@ -63,32 +65,44 @@
|
||||
"react-dom": "^16.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.7.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.7.4",
|
||||
"@babel/preset-env": "7.7.7",
|
||||
"@babel/preset-react": "7.7.4",
|
||||
"@babel/preset-typescript": "7.7.7",
|
||||
"@semantic-release/changelog": "3.0.6",
|
||||
"@semantic-release/git": "7.0.18",
|
||||
"@semantic-release/npm": "5.3.5",
|
||||
"@shopify/jest-dom-mocks": "2.8.7",
|
||||
"@storybook/addon-actions": "5.2.8",
|
||||
"@storybook/addon-knobs": "5.2.8",
|
||||
"@storybook/addon-notes": "5.2.8",
|
||||
"@storybook/addon-options": "5.2.8",
|
||||
"@storybook/react": "5.2.8",
|
||||
"@babel/core": "7.8.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.8.4",
|
||||
"@babel/preset-react": "7.8.3",
|
||||
"@babel/preset-typescript": "7.8.3",
|
||||
"@semantic-release/changelog": "5.0.0",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/npm": "7.0.3",
|
||||
"@shopify/jest-dom-mocks": "2.8.9",
|
||||
"@storybook/addon-actions": "5.3.13",
|
||||
"@storybook/addon-knobs": "5.3.13",
|
||||
"@storybook/addon-notes": "5.3.13",
|
||||
"@storybook/addon-options": "5.3.13",
|
||||
"@storybook/react": "5.3.13",
|
||||
"@testing-library/react": "9.4.0",
|
||||
"@testing-library/react-hooks": "3.2.1",
|
||||
"@types/jest": "24.0.25",
|
||||
"@types/jest": "25.1.2",
|
||||
"@types/react": "16.9.11",
|
||||
"@typescript-eslint/eslint-plugin": "2.16.0",
|
||||
"@typescript-eslint/parser": "2.16.0",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-eslint": "10.0.3",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-plugin-dynamic-import-node": "2.3.0",
|
||||
"fork-ts-checker-webpack-plugin": "3.1.1",
|
||||
"gh-pages": "2.1.1",
|
||||
"husky": "3.1.0",
|
||||
"jest": "24.9.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-react-app": "5.2.0",
|
||||
"eslint-plugin-flowtype": "4.6.0",
|
||||
"eslint-plugin-import": "2.20.1",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-react": "7.18.0",
|
||||
"eslint-plugin-react-hooks": "2.3.0",
|
||||
"fork-ts-checker-webpack-plugin": "4.0.4",
|
||||
"gh-pages": "2.2.0",
|
||||
"husky": "4.2.3",
|
||||
"jest": "25.1.0",
|
||||
"jest-localstorage-mock": "2.4.0",
|
||||
"keyboardjs": "2.5.1",
|
||||
"lint-staged": "9.5.0",
|
||||
"lint-staged": "10.0.7",
|
||||
"markdown-loader": "5.1.0",
|
||||
"prettier": "1.19.1",
|
||||
"raf-stub": "3.0.0",
|
||||
@ -100,18 +114,13 @@
|
||||
"rebound": "0.1.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"rimraf": "3.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"rxjs": "6.5.4",
|
||||
"semantic-release": "15.14.0",
|
||||
"ts-jest": "24.2.0",
|
||||
"semantic-release": "17.0.3",
|
||||
"ts-jest": "25.2.0",
|
||||
"ts-loader": "6.2.1",
|
||||
"ts-node": "8.5.4",
|
||||
"tslint": "6.0.0-beta1",
|
||||
"tslint-config-prettier": "1.18.0",
|
||||
"tslint-eslint-rules": "5.4.0",
|
||||
"tslint-plugin-prettier": "2.1.0",
|
||||
"tslint-react": "4.1.0",
|
||||
"typescript": "3.7.4"
|
||||
"ts-node": "8.6.2",
|
||||
"typescript": "3.7.5"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
@ -139,13 +148,13 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx}": [
|
||||
"tslint --fix -t verbose",
|
||||
"eslint --fix",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"volta": {
|
||||
"node": "10.18.0",
|
||||
"yarn": "1.21.1"
|
||||
"node": "10.19.0",
|
||||
"yarn": "1.22.0"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
@ -157,6 +166,9 @@
|
||||
"coverageDirectory": "coverage",
|
||||
"testMatch": [
|
||||
"<rootDir>/tests/**/*.test.(ts|tsx)"
|
||||
],
|
||||
"setupFiles": [
|
||||
"./tests/setupTests.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
const createBreakpoint = (
|
||||
|
||||
32
src/createGlobalState.ts
Normal file
32
src/createGlobalState.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/* eslint-disable */
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import useEffectOnce from './useEffectOnce';
|
||||
|
||||
export function createGlobalState<S = any>(initialState?: S) {
|
||||
const store: { state: S | undefined; setState: (state: S) => void; setters: any[] } = {
|
||||
state: initialState,
|
||||
setState(state: S) {
|
||||
store.state = state;
|
||||
store.setters.forEach(setter => setter(store.state));
|
||||
},
|
||||
setters: [],
|
||||
};
|
||||
|
||||
return (): [S | undefined, (state: S) => void] => {
|
||||
const [globalState, stateSetter] = useState<S | undefined>(store.state);
|
||||
|
||||
useEffectOnce(() => () => {
|
||||
store.setters = store.setters.filter(setter => setter !== stateSetter);
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!store.setters.includes(stateSetter)) {
|
||||
store.setters.push(stateSetter);
|
||||
}
|
||||
});
|
||||
|
||||
return [globalState, store.setState];
|
||||
};
|
||||
}
|
||||
|
||||
export default createGlobalState;
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const createMemo = <T extends (...args: any) => any>(fn: T) => (...args: Parameters<T>) =>
|
||||
|
||||
26
src/createReducerContext.ts
Normal file
26
src/createReducerContext.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { createFactory, createContext, useContext, useReducer } from 'react';
|
||||
|
||||
const createReducerContext = <R extends React.Reducer<any, any>>(
|
||||
reducer: R,
|
||||
defaultInitialState: React.ReducerState<R>
|
||||
) => {
|
||||
const context = createContext<[React.ReducerState<R>, React.Dispatch<React.ReducerAction<R>>] | undefined>(undefined);
|
||||
const providerFactory = createFactory(context.Provider);
|
||||
|
||||
const ReducerProvider: React.FC<{ initialState?: React.ReducerState<R> }> = ({ children, initialState }) => {
|
||||
const state = useReducer<R>(reducer, initialState !== undefined ? initialState : defaultInitialState);
|
||||
return providerFactory({ value: state }, children);
|
||||
};
|
||||
|
||||
const useReducerContext = () => {
|
||||
const state = useContext(context);
|
||||
if (state == null) {
|
||||
throw new Error(`useReducerContext must be used inside a ReducerProvider.`);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
return [useReducerContext, ReducerProvider, context] as const;
|
||||
};
|
||||
|
||||
export default createReducerContext;
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import * as React from 'react';
|
||||
|
||||
export interface RouterProviderProps {
|
||||
|
||||
23
src/createStateContext.ts
Normal file
23
src/createStateContext.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createFactory, createContext, useContext, useState } from 'react';
|
||||
|
||||
const createStateContext = <T>(defaultInitialValue: T) => {
|
||||
const context = createContext<[T, React.Dispatch<React.SetStateAction<T>>] | undefined>(undefined);
|
||||
const providerFactory = createFactory(context.Provider);
|
||||
|
||||
const StateProvider: React.FC<{ initialValue?: T }> = ({ children, initialValue }) => {
|
||||
const state = useState<T>(initialValue !== undefined ? initialValue : defaultInitialValue);
|
||||
return providerFactory({ value: state }, children);
|
||||
};
|
||||
|
||||
const useStateContext = () => {
|
||||
const state = useContext(context);
|
||||
if (state == null) {
|
||||
throw new Error(`useStateContext must be used inside a StateProvider.`);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
return [useStateContext, StateProvider, context] as const;
|
||||
};
|
||||
|
||||
export default createStateContext;
|
||||
@ -1,5 +1,7 @@
|
||||
export { default as createMemo } from './createMemo';
|
||||
export { default as createReducerContext } from './createReducerContext';
|
||||
export { default as createReducer } from './createReducer';
|
||||
export { default as createStateContext } from './createStateContext';
|
||||
export { default as useAsync } from './useAsync';
|
||||
export { default as useAsyncFn } from './useAsyncFn';
|
||||
export { default as useAsyncRetry } from './useAsyncRetry';
|
||||
@ -8,6 +10,7 @@ export { default as useBattery } from './useBattery';
|
||||
export { default as useBeforeUnload } from './useBeforeUnload';
|
||||
export { default as useBoolean } from './useBoolean';
|
||||
export { default as useClickAway } from './useClickAway';
|
||||
export { default as useCookie } from './useCookie';
|
||||
export { default as useCopyToClipboard } from './useCopyToClipboard';
|
||||
export { default as useCounter } from './useCounter';
|
||||
export { default as useCss } from './useCss';
|
||||
@ -20,6 +23,7 @@ export { default as useDropArea } from './useDropArea';
|
||||
export { default as useEffectOnce } from './useEffectOnce';
|
||||
export { default as useEnsuredForwardedRef, ensuredForwardRef } from './useEnsuredForwardedRef';
|
||||
export { default as useEvent } from './useEvent';
|
||||
export { default as useError } from './useError';
|
||||
export { default as useFavicon } from './useFavicon';
|
||||
export { default as useFullscreen } from './useFullscreen';
|
||||
export { default as useGeolocation } from './useGeolocation';
|
||||
@ -44,6 +48,7 @@ export { default as useLocalStorage } from './useLocalStorage';
|
||||
export { default as useLocation } from './useLocation';
|
||||
export { default as useLockBodyScroll } from './useLockBodyScroll';
|
||||
export { default as useLogger } from './useLogger';
|
||||
export { default as useLongPress } from './useLongPress';
|
||||
export { default as useMap } from './useMap';
|
||||
export { default as useMedia } from './useMedia';
|
||||
export { default as useMediaDevices } from './useMediaDevices';
|
||||
@ -73,6 +78,7 @@ export { default as useSessionStorage } from './useSessionStorage';
|
||||
export { default as useSetState } from './useSetState';
|
||||
export { default as useShallowCompareEffect } from './useShallowCompareEffect';
|
||||
export { default as useSize } from './useSize';
|
||||
export { default as useSlider } from './useSlider';
|
||||
export { default as useSpeech } from './useSpeech';
|
||||
// not exported because of peer dependency
|
||||
// export { default as useSpring } from './useSpring';
|
||||
@ -102,3 +108,4 @@ export { default as useMeasure } from './useMeasure';
|
||||
export { useRendersCount } from './useRendersCount';
|
||||
export { useFirstMountState } from './useFirstMountState';
|
||||
export { default as useSet } from './useSet';
|
||||
export { createGlobalState } from './createGlobalState';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { DependencyList, useCallback, useState, useRef } from 'react';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { DependencyList, useCallback, useState } from 'react';
|
||||
import useAsync, { AsyncState } from './useAsync';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* eslint-disable */
|
||||
import * as React from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { off, on } from './util';
|
||||
import { off, on, isDeepEqual } from './util';
|
||||
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
@ -53,7 +53,7 @@ function useBattery(): UseBatteryState {
|
||||
dischargingTime: battery.dischargingTime,
|
||||
chargingTime: battery.chargingTime,
|
||||
};
|
||||
!isEqual(state, newState) && setState(newState);
|
||||
!isDeepEqual(state, newState) && setState(newState);
|
||||
};
|
||||
|
||||
nav!.getBattery!().then((bat: BatteryManager) => {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
const useBeforeUnload = (enabled: boolean = true, message?: string) => {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const useBeforeUnload = (enabled: boolean | (() => boolean) = true, message?: string) => {
|
||||
const handler = useCallback(
|
||||
(event: BeforeUnloadEvent) => {
|
||||
const finalEnabled = typeof enabled === 'function' ? enabled() : true;
|
||||
|
||||
if (!finalEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = (event: BeforeUnloadEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (message) {
|
||||
@ -14,12 +16,19 @@ const useBeforeUnload = (enabled: boolean = true, message?: string) => {
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
},
|
||||
[enabled, message]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', handler);
|
||||
|
||||
return () => window.removeEventListener('beforeunload', handler);
|
||||
}, [message, enabled]);
|
||||
}, [enabled, handler]);
|
||||
};
|
||||
|
||||
export default useBeforeUnload;
|
||||
|
||||
25
src/useCookie.ts
Normal file
25
src/useCookie.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const useCookie = (
|
||||
cookieName: string
|
||||
): [string | null, (newValue: string, options?: Cookies.CookieAttributes) => void, () => void] => {
|
||||
const [value, setValue] = useState<string | null>(() => Cookies.get(cookieName) || null);
|
||||
|
||||
const updateCookie = useCallback(
|
||||
(newValue: string, options?: Cookies.CookieAttributes) => {
|
||||
Cookies.set(cookieName, newValue, options);
|
||||
setValue(newValue);
|
||||
},
|
||||
[cookieName]
|
||||
);
|
||||
|
||||
const deleteCookie = useCallback(() => {
|
||||
Cookies.remove(cookieName);
|
||||
setValue(null);
|
||||
}, [cookieName]);
|
||||
|
||||
return [value, updateCookie, deleteCookie];
|
||||
};
|
||||
|
||||
export default useCookie;
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import writeText from 'copy-to-clipboard';
|
||||
import { useCallback } from 'react';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useMemo } from 'react';
|
||||
import useGetSet from './useGetSet';
|
||||
import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DependencyList, EffectCallback } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { isDeepEqual } from './util';
|
||||
import useCustomCompareEffect from './useCustomCompareEffect';
|
||||
|
||||
const isPrimitive = (val: any) => val !== Object(val);
|
||||
@ -17,7 +17,7 @@ const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
|
||||
}
|
||||
}
|
||||
|
||||
useCustomCompareEffect(effect, deps, isEqual);
|
||||
useCustomCompareEffect(effect, deps, isDeepEqual);
|
||||
};
|
||||
|
||||
export default useDeepCompareEffect;
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
const useDefault = (defaultValue, initialValue): [any, (nextValue?: any) => void] => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const useDefault = <TStateType>(defaultValue: TStateType, initialValue: TStateType | (() => TStateType)) => {
|
||||
const [value, setValue] = useState<TStateType | undefined | null>(initialValue);
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return [defaultValue, setValue];
|
||||
return [defaultValue, setValue] as const;
|
||||
}
|
||||
|
||||
return [value, setValue];
|
||||
return [value, setValue] as const;
|
||||
};
|
||||
|
||||
export default useDefault;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import * as React from 'react';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useMemo, useState } from 'react';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
|
||||
19
src/useError.ts
Normal file
19
src/useError.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
const useError = (): ((err: Error) => void) => {
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const dispatchError = useCallback((err: Error) => {
|
||||
setError(err);
|
||||
}, []);
|
||||
|
||||
return dispatchError;
|
||||
};
|
||||
|
||||
export default useError;
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react';
|
||||
import { isClient } from './util';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { RefObject, useLayoutEffect, useState } from 'react';
|
||||
import screenfull from 'screenfull';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface GeoLocationSensorState {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { Dispatch, useMemo, useRef } from 'react';
|
||||
import useUpdate from './useUpdate';
|
||||
import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useCallback, useRef } from 'react';
|
||||
import useUpdate from './useUpdate';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// kudos: https://usehooks.com/
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import { off, on } from './util';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { RefObject, useEffect, useState } from 'react';
|
||||
|
||||
const useIntersection = (
|
||||
@ -7,7 +8,7 @@ const useIntersection = (
|
||||
const [intersectionObserverEntry, setIntersectionObserverEntry] = useState<IntersectionObserverEntry | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
if (ref.current && typeof IntersectionObserver === 'function') {
|
||||
const handler = (entries: IntersectionObserverEntry[]) => {
|
||||
setIntersectionObserverEntry(entries[0]);
|
||||
};
|
||||
@ -16,13 +17,12 @@ const useIntersection = (
|
||||
observer.observe(ref.current);
|
||||
|
||||
return () => {
|
||||
if (ref.current) {
|
||||
observer.disconnect();
|
||||
}
|
||||
setIntersectionObserverEntry(null);
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, [ref, options.threshold, options.root, options.rootMargin]);
|
||||
}, [ref.current, options.threshold, options.root, options.rootMargin]);
|
||||
|
||||
return intersectionObserverEntry;
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { DependencyList, useMemo } from 'react';
|
||||
import useEvent, { UseEventTarget } from './useEvent';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useLifecycles = (mount, unmount?) => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useUpdate from './useUpdate';
|
||||
import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState';
|
||||
|
||||
@ -1,22 +1,38 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isClient } from './util';
|
||||
|
||||
type Dispatch<A> = (value: A) => void;
|
||||
type SetStateAction<S> = S | ((prevState: S) => S);
|
||||
type parserOptions<T> =
|
||||
| {
|
||||
raw: true;
|
||||
}
|
||||
| {
|
||||
raw: false;
|
||||
serializer: (value: T) => string;
|
||||
deserializer: (value: string) => T;
|
||||
};
|
||||
|
||||
const useLocalStorage = <T>(key: string, initialValue?: T, raw?: boolean): [T, Dispatch<SetStateAction<T>>] => {
|
||||
const useLocalStorage = <T>(
|
||||
key: string,
|
||||
initialValue?: T,
|
||||
options?: parserOptions<T>
|
||||
): [T, React.Dispatch<React.SetStateAction<T>>] => {
|
||||
if (!isClient) {
|
||||
return [initialValue as T, () => {}];
|
||||
}
|
||||
|
||||
// Use provided serializer/deserializer or the default ones
|
||||
const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;
|
||||
const deserializer = options ? (options.raw ? String : options.deserializer) : JSON.parse;
|
||||
|
||||
const [state, setState] = useState<T>(() => {
|
||||
try {
|
||||
const localStorageValue = localStorage.getItem(key);
|
||||
if (typeof localStorageValue !== 'string') {
|
||||
localStorage.setItem(key, raw ? String(initialValue) : JSON.stringify(initialValue));
|
||||
return initialValue;
|
||||
if (localStorageValue !== null) {
|
||||
return deserializer(localStorageValue);
|
||||
} else {
|
||||
return raw ? localStorageValue : JSON.parse(localStorageValue || 'null');
|
||||
initialValue && localStorage.setItem(key, serializer(initialValue));
|
||||
return initialValue;
|
||||
}
|
||||
} catch {
|
||||
// If user is in private mode or has storage restriction
|
||||
@ -28,8 +44,7 @@ const useLocalStorage = <T>(key: string, initialValue?: T, raw?: boolean): [T, D
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const serializedState = raw ? String(state) : JSON.stringify(state);
|
||||
localStorage.setItem(key, serializedState);
|
||||
localStorage.setItem(key, serializer(state));
|
||||
} catch {
|
||||
// If user is in private mode or has storage restriction
|
||||
// localStorage can throw. Also JSON.stringify can throw.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isClient, off, on } from './util';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { RefObject, useEffect, useRef } from 'react';
|
||||
|
||||
export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | null): HTMLElement | null {
|
||||
@ -15,15 +16,33 @@ export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | n
|
||||
return getClosestBody((el as HTMLElement).offsetParent!);
|
||||
}
|
||||
|
||||
function preventDefault(rawEvent: TouchEvent): boolean {
|
||||
const e = rawEvent || window.event;
|
||||
// Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
|
||||
if (e.touches.length > 1) return true;
|
||||
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface BodyInfoItem {
|
||||
counter: number;
|
||||
initialOverflow: CSSStyleDeclaration['overflow'];
|
||||
}
|
||||
|
||||
const isIosDevice =
|
||||
typeof window !== 'undefined' &&
|
||||
window.navigator &&
|
||||
window.navigator.platform &&
|
||||
/iP(ad|hone|od)/.test(window.navigator.platform);
|
||||
|
||||
const bodies: Map<HTMLElement, BodyInfoItem> = new Map();
|
||||
|
||||
const doc: Document | undefined = typeof document === 'object' ? document : undefined;
|
||||
|
||||
let documentListenerAdded = false;
|
||||
|
||||
export default !doc
|
||||
? function useLockBodyMock(_locked: boolean = true, _elementRef?: RefObject<HTMLElement>) {}
|
||||
: function useLockBody(locked: boolean = true, elementRef?: RefObject<HTMLElement>) {
|
||||
@ -40,7 +59,15 @@ export default !doc
|
||||
if (locked) {
|
||||
if (!bodyInfo) {
|
||||
bodies.set(body, { counter: 1, initialOverflow: body.style.overflow });
|
||||
body.style.overflow = 'hidden';
|
||||
if (isIosDevice) {
|
||||
if (!documentListenerAdded) {
|
||||
document.addEventListener('touchmove', preventDefault, { passive: false });
|
||||
|
||||
documentListenerAdded = true;
|
||||
}
|
||||
} else {
|
||||
body.style.overflow = 'hidden';
|
||||
}
|
||||
} else {
|
||||
bodies.set(body, { counter: bodyInfo.counter + 1, initialOverflow: bodyInfo.initialOverflow });
|
||||
}
|
||||
@ -48,7 +75,16 @@ export default !doc
|
||||
if (bodyInfo) {
|
||||
if (bodyInfo.counter === 1) {
|
||||
bodies.delete(body);
|
||||
body.style.overflow = bodyInfo.initialOverflow;
|
||||
if (isIosDevice) {
|
||||
body.ontouchmove = null;
|
||||
|
||||
if (documentListenerAdded) {
|
||||
document.removeEventListener('touchmove', preventDefault);
|
||||
documentListenerAdded = false;
|
||||
}
|
||||
} else {
|
||||
body.style.overflow = bodyInfo.initialOverflow;
|
||||
}
|
||||
} else {
|
||||
bodies.set(body, { counter: bodyInfo.counter - 1, initialOverflow: bodyInfo.initialOverflow });
|
||||
}
|
||||
|
||||
58
src/useLongPress.ts
Normal file
58
src/useLongPress.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/* eslint-disable */
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
interface Options {
|
||||
isPreventDefault?: boolean;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const isTouchEvent = (event: Event): event is TouchEvent => {
|
||||
return 'touches' in event;
|
||||
};
|
||||
|
||||
const preventDefault = (event: Event) => {
|
||||
if (!isTouchEvent(event)) return;
|
||||
|
||||
if (event.touches.length < 2 && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const useLongPress = (
|
||||
callback: (e: TouchEvent | MouseEvent) => void,
|
||||
{ isPreventDefault = true, delay = 300 }: Options = {}
|
||||
) => {
|
||||
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const target = useRef<EventTarget>();
|
||||
|
||||
const start = useCallback(
|
||||
(event: TouchEvent | MouseEvent) => {
|
||||
// prevent ghost click on mobile devices
|
||||
if (isPreventDefault && event.target) {
|
||||
event.target.addEventListener('touchend', preventDefault, { passive: false });
|
||||
target.current = event.target;
|
||||
}
|
||||
timeout.current = setTimeout(() => callback(event), delay);
|
||||
},
|
||||
[callback, delay]
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
// clearTimeout and removeEventListener
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
|
||||
if (isPreventDefault && target.current) {
|
||||
target.current.removeEventListener('touchend', preventDefault);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
onMouseDown: (e: any) => start(e),
|
||||
onTouchStart: (e: any) => start(e),
|
||||
onMouseUp: clear,
|
||||
onMouseLeave: clear,
|
||||
onTouchEnd: clear,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default useLongPress;
|
||||
@ -1,7 +1,9 @@
|
||||
/* eslint-disable */
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
|
||||
export interface StableActions<T extends object> {
|
||||
set: <K extends keyof T>(key: K, value: T[K]) => void;
|
||||
setAll: (newMap: T) => void;
|
||||
remove: <K extends keyof T>(key: K) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
@ -21,6 +23,9 @@ const useMap = <T extends object = any>(initialMap: T = {} as T): [T, Actions<T>
|
||||
[key]: entry,
|
||||
}));
|
||||
},
|
||||
setAll: (newMap: T) => {
|
||||
set(newMap);
|
||||
},
|
||||
remove: key => {
|
||||
set(prevMap => {
|
||||
const { [key]: omit, ...rest } = prevMap;
|
||||
|
||||
51
src/useMeasureDirty.ts
Normal file
51
src/useMeasureDirty.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/* eslint-disable */
|
||||
import { useState, useEffect, useRef, RefObject } from 'react';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
export interface ContentRect {
|
||||
width: number;
|
||||
height: number;
|
||||
top: number;
|
||||
right: number;
|
||||
left: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
const useMeasureDirty = (ref: RefObject<HTMLElement>): ContentRect => {
|
||||
const frame = useRef(0);
|
||||
const [rect, set] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
});
|
||||
|
||||
const [observer] = useState(
|
||||
() =>
|
||||
new ResizeObserver(entries => {
|
||||
const entry = entries[0];
|
||||
|
||||
if (entry) {
|
||||
cancelAnimationFrame(frame.current);
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
set(entry.contentRect);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
observer.disconnect();
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
return rect;
|
||||
};
|
||||
|
||||
export default useMeasureDirty;
|
||||
@ -34,4 +34,6 @@ const useMediaDevices = () => {
|
||||
return state;
|
||||
};
|
||||
|
||||
export default useMediaDevices;
|
||||
const useMediaDevicesMock = () => ({});
|
||||
|
||||
export default typeof navigator === 'object' && !!navigator.mediaDevices ? useMediaDevices : useMediaDevicesMock;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react';
|
||||
|
||||
export interface StateMediator<S = any> {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useMemo, useReducer } from 'react';
|
||||
|
||||
const useMethods = (createMethods, initialState) => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
import useRafState from './useRafState';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { StateValidator, UseStateValidatorReturn, ValidityState } from './useStateValidator';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { off, on } from './util';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { off, on } from './util';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const usePageLeave = (onPageLeave, args = []) => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { off, on } from './util';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useCallback } from 'react';
|
||||
import useMountedState from './useMountedState';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type RafLoopReturns = [() => void, boolean, () => void];
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
import useRafState from './useRafState';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { scrollbarWidth } from '@xobotyi/scrollbar-width';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { RefObject, useEffect, useState } from 'react';
|
||||
|
||||
const useScrolling = (ref: RefObject<HTMLElement>): boolean => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const getValue = (search: string, param: string) => new URLSearchParams(search).get(param);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isClient } from './util';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
|
||||
export interface StableActions<K> {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import * as React from 'react';
|
||||
import { isClient } from './util';
|
||||
|
||||
|
||||
142
src/useSlider.ts
Normal file
142
src/useSlider.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useRef, RefObject, CSSProperties } from 'react';
|
||||
|
||||
import { isClient, off, on } from './util';
|
||||
import useMountedState from './useMountedState';
|
||||
import useSetState from './useSetState';
|
||||
|
||||
export interface State {
|
||||
isSliding: boolean;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
onScrub: (value: number) => void;
|
||||
onScrubStart: () => void;
|
||||
onScrubStop: () => void;
|
||||
reverse: boolean;
|
||||
styles: boolean | CSSProperties;
|
||||
vertical?: boolean;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const useSlider = (ref: RefObject<HTMLElement>, options: Partial<Options> = {}): State => {
|
||||
const isMounted = useMountedState();
|
||||
const isSliding = useRef(false);
|
||||
const frame = useRef(0);
|
||||
const [state, setState] = useSetState<State>({
|
||||
isSliding: false,
|
||||
value: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isClient) {
|
||||
const styles = options.styles === undefined ? true : options.styles;
|
||||
const reverse = options.reverse === undefined ? false : options.reverse;
|
||||
|
||||
if (ref.current && styles) {
|
||||
ref.current.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
const startScrubbing = () => {
|
||||
if (!isSliding.current && isMounted()) {
|
||||
(options.onScrubStart || noop)();
|
||||
isSliding.current = true;
|
||||
setState({ isSliding: true });
|
||||
bindEvents();
|
||||
}
|
||||
};
|
||||
|
||||
const stopScrubbing = () => {
|
||||
if (isSliding.current && isMounted()) {
|
||||
(options.onScrubStop || noop)();
|
||||
isSliding.current = false;
|
||||
setState({ isSliding: false });
|
||||
unbindEvents();
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
startScrubbing();
|
||||
onMouseMove(event);
|
||||
};
|
||||
const onMouseMove = options.vertical
|
||||
? (event: MouseEvent) => onScrub(event.clientY)
|
||||
: (event: MouseEvent) => onScrub(event.clientX);
|
||||
|
||||
const onTouchStart = (event: TouchEvent) => {
|
||||
startScrubbing();
|
||||
onTouchMove(event);
|
||||
};
|
||||
const onTouchMove = options.vertical
|
||||
? (event: TouchEvent) => onScrub(event.changedTouches[0].clientY)
|
||||
: (event: TouchEvent) => onScrub(event.changedTouches[0].clientX);
|
||||
|
||||
const bindEvents = () => {
|
||||
on(document, 'mousemove', onMouseMove);
|
||||
on(document, 'mouseup', stopScrubbing);
|
||||
|
||||
on(document, 'touchmove', onTouchMove);
|
||||
on(document, 'touchend', stopScrubbing);
|
||||
};
|
||||
|
||||
const unbindEvents = () => {
|
||||
off(document, 'mousemove', onMouseMove);
|
||||
off(document, 'mouseup', stopScrubbing);
|
||||
|
||||
off(document, 'touchmove', onTouchMove);
|
||||
off(document, 'touchend', stopScrubbing);
|
||||
};
|
||||
|
||||
const onScrub = (clientXY: number) => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
if (isMounted() && ref.current) {
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const pos = options.vertical ? rect.top : rect.left;
|
||||
const length = options.vertical ? rect.height : rect.width;
|
||||
|
||||
// Prevent returning 0 when element is hidden by CSS
|
||||
if (!length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = (clientXY - pos) / length;
|
||||
|
||||
if (value > 1) {
|
||||
value = 1;
|
||||
} else if (value < 0) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
if (reverse) {
|
||||
value = 1 - value;
|
||||
}
|
||||
|
||||
setState({
|
||||
value,
|
||||
});
|
||||
|
||||
(options.onScrub || noop)(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
on(ref.current, 'mousedown', onMouseDown);
|
||||
on(ref.current, 'touchstart', onTouchStart);
|
||||
|
||||
return () => {
|
||||
off(ref.current, 'mousedown', onMouseDown);
|
||||
off(ref.current, 'touchstart', onTouchStart);
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [ref, options.vertical]);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default useSlider;
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Spring, SpringSystem } from 'rebound';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
const isFocusedElementEditable = () => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useMemo, useRef } from 'react';
|
||||
import useMountedState from './useMountedState';
|
||||
import useUpdate from './useUpdate';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type ValidityState = [boolean | undefined, ...any[]];
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { Dispatch, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useFirstMountState } from './useFirstMountState';
|
||||
import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import useUnmount from './useUnmount';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import useUnmount from './useUnmount';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
function useTitle(title: string) {
|
||||
const t = useRef<string>();
|
||||
|
||||
if (t.current !== title) {
|
||||
document.title = t.current = title;
|
||||
}
|
||||
/* eslint-disable */
|
||||
import { useRef, useEffect } from 'react';
|
||||
export interface UseTitleOptions {
|
||||
restoreOnUnmount?: boolean;
|
||||
}
|
||||
const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = {
|
||||
restoreOnUnmount: false,
|
||||
};
|
||||
function useTitle(title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS) {
|
||||
const prevTitleRef = useRef(document.title);
|
||||
document.title = title;
|
||||
useEffect(() => {
|
||||
if (options && options.restoreOnUnmount) {
|
||||
return () => {
|
||||
document.title = prevTitleRef.current;
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default typeof document !== 'undefined' ? useTitle : (_title: string) => {};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react';
|
||||
import { useFirstMountState } from './useFirstMountState';
|
||||
|
||||
@ -5,7 +6,9 @@ const useUpdateEffect: typeof useEffect = (effect, deps) => {
|
||||
const isFirstMount = useFirstMountState();
|
||||
|
||||
useEffect(() => {
|
||||
!isFirstMount && effect();
|
||||
if (!isFirstMount) {
|
||||
return effect();
|
||||
}
|
||||
}, deps);
|
||||
};
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export type VibrationPattern = number | number[];
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react';
|
||||
import { isClient } from './util';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import useRafState from './useRafState';
|
||||
|
||||
@ -3,3 +3,5 @@ export const isClient = typeof window === 'object';
|
||||
export const on = (obj: any, ...args: any[]) => obj.addEventListener(...args);
|
||||
|
||||
export const off = (obj: any, ...args: any[]) => obj.removeEventListener(...args);
|
||||
|
||||
export const isDeepEqual: (a: any, b: any) => boolean = require('fast-deep-equal/react');
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import useSetState from '../useSetState';
|
||||
|
||||
33
stories/createGlobalState.story.tsx
Normal file
33
stories/createGlobalState.story.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React, { FC } from "react";
|
||||
import { createGlobalState } from "../src";
|
||||
import ShowDocs from "./util/ShowDocs";
|
||||
|
||||
const useGlobalValue = createGlobalState<number>(0);
|
||||
|
||||
const CompA: FC = () => {
|
||||
const [value, setValue] = useGlobalValue();
|
||||
|
||||
return <button onClick={() => setValue(value + 1)}>+</button>;
|
||||
};
|
||||
|
||||
const CompB: FC = () => {
|
||||
const [value, setValue] = useGlobalValue();
|
||||
|
||||
return <button onClick={() => setValue(value - 1)}>-</button>;
|
||||
};
|
||||
|
||||
const Demo: FC = () => {
|
||||
const [value] = useGlobalValue();
|
||||
return (
|
||||
<div>
|
||||
<p>{value}</p>
|
||||
<CompA />
|
||||
<CompB />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("State|createGlobalState", module)
|
||||
.add("Docs", () => <ShowDocs md={require("../docs/createGlobalState.md")} />)
|
||||
.add("Demo", () => <Demo />);
|
||||
66
stories/createReducerContext.story.tsx
Normal file
66
stories/createReducerContext.story.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { createReducerContext } from '../src';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
type Action = 'increment' | 'decrement';
|
||||
|
||||
const reducer = (state: number, action: Action) => {
|
||||
switch (action) {
|
||||
case 'increment':
|
||||
return state + 1;
|
||||
case 'decrement':
|
||||
return state - 1;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
const [useSharedCounter, SharedCounterProvider] = createReducerContext(reducer, 0);
|
||||
|
||||
const ComponentA = () => {
|
||||
const [count, dispatch] = useSharedCounter();
|
||||
return (
|
||||
<p>
|
||||
Component A
|
||||
<button type="button" onClick={() => dispatch('decrement')}>
|
||||
-
|
||||
</button>
|
||||
{count}
|
||||
<button type="button" onClick={() => dispatch('increment')}>
|
||||
+
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const ComponentB = () => {
|
||||
const [count, dispatch] = useSharedCounter();
|
||||
return (
|
||||
<p>
|
||||
Component B
|
||||
<button type="button" onClick={() => dispatch('decrement')}>
|
||||
-
|
||||
</button>
|
||||
{count}
|
||||
<button type="button" onClick={() => dispatch('increment')}>
|
||||
+
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<SharedCounterProvider>
|
||||
<p>Those two counters share the same value.</p>
|
||||
<ComponentA />
|
||||
<ComponentB />
|
||||
</SharedCounterProvider>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('State|createReducerContext', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../docs/createReducerContext.md')} />)
|
||||
.add('Demo', () => <Demo />);
|
||||
43
stories/createStateContext.story.tsx
Normal file
43
stories/createStateContext.story.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { createStateContext } from '../src';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const [useSharedText, SharedTextProvider] = createStateContext('');
|
||||
|
||||
const ComponentA = () => {
|
||||
const [text, setText] = useSharedText();
|
||||
return (
|
||||
<p>
|
||||
Component A:
|
||||
<br />
|
||||
<input type="text" value={text} onInput={ev => setText(ev.currentTarget.value)} />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const ComponentB = () => {
|
||||
const [text, setText] = useSharedText();
|
||||
return (
|
||||
<p>
|
||||
Component B:
|
||||
<br />
|
||||
<input type="text" value={text} onInput={ev => setText(ev.currentTarget.value)} />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<SharedTextProvider>
|
||||
<p>Those two fields share the same value.</p>
|
||||
<ComponentA />
|
||||
<ComponentB />
|
||||
</SharedTextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('State|createStateContext', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../docs/createStateContext.md')} />)
|
||||
.add('Demo', () => <Demo />);
|
||||
@ -1,9 +1,9 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useBeforeUnload, useToggle } from '../src';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
const DemoBool = () => {
|
||||
const [dirty, toggleDirty] = useToggle(false);
|
||||
useBeforeUnload(dirty, 'You have unsaved changes, are you sure?');
|
||||
|
||||
@ -15,6 +15,22 @@ const Demo = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const DemoFunc = () => {
|
||||
const [dirty, toggleDirty] = useToggle(false);
|
||||
const dirtyFn = useCallback(() => {
|
||||
return dirty;
|
||||
}, [dirty]);
|
||||
useBeforeUnload(dirtyFn, 'You have unsaved changes, are you sure?');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{dirty && <p>Try to reload or close tab</p>}
|
||||
<button onClick={() => toggleDirty()}>{dirty ? 'Disable' : 'Enable'}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('Side effects|useBeforeUnload', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../docs/useBeforeUnload.md')} />)
|
||||
.add('Demo', () => <Demo />);
|
||||
.add('Demo (boolean)', () => <DemoBool />)
|
||||
.add('Demo (function)', () => <DemoFunc />);
|
||||
|
||||
31
stories/useCookie.story.tsx
Normal file
31
stories/useCookie.story.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useCookie } from "../src";
|
||||
import ShowDocs from "./util/ShowDocs";
|
||||
|
||||
const Demo = () => {
|
||||
const [value, updateCookie, deleteCookie] = useCookie("my-cookie");
|
||||
const [counter, setCounter] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
deleteCookie();
|
||||
}, []);
|
||||
|
||||
const updateCookieHandler = () => {
|
||||
updateCookie(`my-awesome-cookie-${counter}`);
|
||||
setCounter(c => c + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Value: {value}</p>
|
||||
<button onClick={updateCookieHandler}>Update Cookie</button>
|
||||
<br />
|
||||
<button onClick={deleteCookie}>Delete Cookie</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Side effects|useCookie", module)
|
||||
.add("Docs", () => <ShowDocs md={require("../docs/useCookie.md")} />)
|
||||
.add("Demo", () => <Demo />);
|
||||
@ -1,7 +1,7 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import { useCounter, useCustomCompareEffect } from '../src';
|
||||
import isDeepEqual from 'react-fast-compare';
|
||||
import { isDeepEqual } from '../src/util';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
|
||||
46
stories/useError.story.tsx
Normal file
46
stories/useError.story.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { useError } from '../src';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Something went wrong.</h1>
|
||||
<button onClick={() => this.setState({ hasError: false })}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const Demo = () => {
|
||||
const dispatchError = useError();
|
||||
|
||||
const clickHandler = () => {
|
||||
dispatchError(new Error('Some error!'));
|
||||
};
|
||||
|
||||
return <button onClick={clickHandler}>Click me to throw</button>;
|
||||
};
|
||||
|
||||
storiesOf('Side effects|useError', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../docs/useError.md')} />)
|
||||
.add('Demo', () => (
|
||||
<ErrorBoundary>
|
||||
<Demo />
|
||||
</ErrorBoundary>
|
||||
));
|
||||
22
stories/useLongPress.story.tsx
Normal file
22
stories/useLongPress.story.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import { useLongPress } from '../src';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
const onLongPress = () => {
|
||||
console.log('calls callback after long pressing 300ms');
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
isPreventDefault: true,
|
||||
delay: 300,
|
||||
};
|
||||
const longPressEvent = useLongPress(onLongPress, defaultOptions);
|
||||
|
||||
return <button {...longPressEvent}>useLongPress</button>;
|
||||
};
|
||||
|
||||
storiesOf('Sensors|useLongPress', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../docs/useLongPress.md')} />)
|
||||
.add('Demo', () => <Demo />);
|
||||
43
stories/useSlider.story.tsx
Normal file
43
stories/useSlider.story.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import { useSlider } from '../src';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
const ref = React.useRef(null);
|
||||
const state = useSlider(ref);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={ref} style={{ position: 'relative', background: 'yellow', padding: 4 }}>
|
||||
<p style={{ margin: 0, textAlign: 'center' }}>Slide me</p>
|
||||
<div style={{ position: 'absolute', top: 0, left: (100 * state.value) + '%', transform: 'scale(2)' }}>
|
||||
{state.isSliding ? '🏂' : '🎿'}
|
||||
</div>
|
||||
</div>
|
||||
<pre>{JSON.stringify(state, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DemoVertical = () => {
|
||||
const ref = React.useRef(null);
|
||||
const state = useSlider(ref, { vertical: true });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={ref} style={{ position: 'relative', background: 'yellow', padding: 4, width: 30, height: 400 }}>
|
||||
<p style={{ margin: 0, textAlign: 'center' }}>Slide me</p>
|
||||
<div style={{ position: 'absolute', left: 0, top: (100 * state.value) + '%', transform: 'scale(2)' }}>
|
||||
{state.isSliding ? '🏂' : '🎿'}
|
||||
</div>
|
||||
</div>
|
||||
<pre>{JSON.stringify(state, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('UI|useSlider', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../docs/useSlider.md')} />)
|
||||
.add('Horizontal', () => <Demo />)
|
||||
.add('Vertical', () => <DemoVertical />);
|
||||
21
tests/createGlobalState.test.ts
Normal file
21
tests/createGlobalState.test.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import createGlobalState from '../src/createGlobalState';
|
||||
|
||||
describe('useGlobalState', () => {
|
||||
it('should be defined', () => {
|
||||
expect(createGlobalState).toBeDefined();
|
||||
});
|
||||
|
||||
it('both components should be updated', () => {
|
||||
const useGlobalValue = createGlobalState(0);
|
||||
const { result: result1 } = renderHook(() => useGlobalValue());
|
||||
const { result: result2 } = renderHook(() => useGlobalValue());
|
||||
expect(result1.current[0] === 0);
|
||||
expect(result2.current[0] === 0);
|
||||
act(() => {
|
||||
result1.current[1](1);
|
||||
});
|
||||
expect(result1.current[0] === 1);
|
||||
expect(result2.current[0] === 1);
|
||||
});
|
||||
});
|
||||
151
tests/createReducerContext.test.tsx
Normal file
151
tests/createReducerContext.test.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import createReducerContext from '../src/createReducerContext';
|
||||
|
||||
type Action = 'increment' | 'decrement';
|
||||
|
||||
const reducer = (state: number, action: Action) => {
|
||||
switch (action) {
|
||||
case 'increment':
|
||||
return state + 1;
|
||||
case 'decrement':
|
||||
return state - 1;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
it('should create a hook and a provider', () => {
|
||||
const [useSharedNumber, SharedNumberProvider] = createReducerContext(reducer, 0);
|
||||
expect(useSharedNumber).toBeInstanceOf(Function);
|
||||
expect(SharedNumberProvider).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
describe('when using created hook', () => {
|
||||
it('should throw out of a provider', () => {
|
||||
const [useSharedNumber] = createReducerContext(reducer, 0);
|
||||
const { result } = renderHook(() => useSharedNumber());
|
||||
expect(result.error).toEqual(new Error('useReducerContext must be used inside a ReducerProvider.'));
|
||||
});
|
||||
|
||||
const setUp = () => {
|
||||
const [useSharedNumber, SharedNumberProvider] = createReducerContext(reducer, 0);
|
||||
const wrapper: React.FC = ({ children }) => <SharedNumberProvider>{children}</SharedNumberProvider>;
|
||||
return renderHook(() => useSharedNumber(), { wrapper });
|
||||
};
|
||||
|
||||
it('should init state and updater', () => {
|
||||
const { result } = setUp();
|
||||
const [sharedNumber, updateSharedNumber] = result.current;
|
||||
|
||||
expect(sharedNumber).toEqual(0);
|
||||
expect(updateSharedNumber).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should update the state', () => {
|
||||
const { result } = setUp();
|
||||
const [, updateSharedNumber] = result.current;
|
||||
|
||||
act(() => updateSharedNumber('increment'));
|
||||
|
||||
const [sharedNumber] = result.current;
|
||||
|
||||
expect(sharedNumber).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using among multiple components', () => {
|
||||
const [useSharedNumber, SharedNumberProvider] = createReducerContext(reducer, 0);
|
||||
|
||||
const DisplayComponent = () => {
|
||||
const [sharedNumber] = useSharedNumber();
|
||||
return <p>{sharedNumber}</p>;
|
||||
};
|
||||
|
||||
const UpdateComponent = () => {
|
||||
const [, updateSharedNumber] = useSharedNumber();
|
||||
return (
|
||||
<button type="button" onClick={() => updateSharedNumber('increment')}>
|
||||
INCREMENT
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
it('should be in sync when under the same provider', () => {
|
||||
const { baseElement, getByText } = render(
|
||||
<SharedNumberProvider>
|
||||
<DisplayComponent />
|
||||
<DisplayComponent />
|
||||
<UpdateComponent />
|
||||
</SharedNumberProvider>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>0</p><p>0</p><button type="button">INCREMENT</button></div>');
|
||||
|
||||
fireEvent.click(getByText('INCREMENT'));
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>1</p><p>1</p><button type="button">INCREMENT</button></div>');
|
||||
});
|
||||
|
||||
it('should be in update independently when under different providers', () => {
|
||||
const { baseElement, getByText } = render(
|
||||
<>
|
||||
<SharedNumberProvider>
|
||||
<DisplayComponent />
|
||||
</SharedNumberProvider>
|
||||
<SharedNumberProvider>
|
||||
<DisplayComponent />
|
||||
<UpdateComponent />
|
||||
</SharedNumberProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>0</p><p>0</p><button type="button">INCREMENT</button></div>');
|
||||
|
||||
fireEvent.click(getByText('INCREMENT'));
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>0</p><p>1</p><button type="button">INCREMENT</button></div>');
|
||||
});
|
||||
|
||||
it('should not update component that do not use the state context', () => {
|
||||
let renderCount = 0;
|
||||
const StaticComponent = () => {
|
||||
renderCount++;
|
||||
return <p>static</p>;
|
||||
};
|
||||
|
||||
const { baseElement, getByText } = render(
|
||||
<>
|
||||
<SharedNumberProvider>
|
||||
<StaticComponent />
|
||||
<DisplayComponent />
|
||||
<UpdateComponent />
|
||||
</SharedNumberProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>static</p><p>0</p><button type="button">INCREMENT</button></div>');
|
||||
|
||||
fireEvent.click(getByText('INCREMENT'));
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>static</p><p>1</p><button type="button">INCREMENT</button></div>');
|
||||
|
||||
expect(renderCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should override initialValue', () => {
|
||||
const { baseElement } = render(
|
||||
<>
|
||||
<SharedNumberProvider>
|
||||
<DisplayComponent />
|
||||
</SharedNumberProvider>
|
||||
<SharedNumberProvider initialState={15}>
|
||||
<DisplayComponent />
|
||||
</SharedNumberProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>0</p><p>15</p></div>');
|
||||
});
|
||||
});
|
||||
138
tests/createStateContext.test.tsx
Normal file
138
tests/createStateContext.test.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import createStateContext from '../src/createStateContext';
|
||||
|
||||
it('should create a hook and a provider', () => {
|
||||
const [useSharedNumber, SharedNumberProvider] = createStateContext(0);
|
||||
expect(useSharedNumber).toBeInstanceOf(Function);
|
||||
expect(SharedNumberProvider).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
describe('when using created hook', () => {
|
||||
it('should throw out of a provider', () => {
|
||||
const [useSharedText] = createStateContext('init');
|
||||
const { result } = renderHook(() => useSharedText());
|
||||
expect(result.error).toEqual(new Error('useStateContext must be used inside a StateProvider.'));
|
||||
});
|
||||
|
||||
const setUp = () => {
|
||||
const [useSharedText, SharedTextProvider] = createStateContext('init');
|
||||
const wrapper: React.FC = ({ children }) => <SharedTextProvider>{children}</SharedTextProvider>;
|
||||
return renderHook(() => useSharedText(), { wrapper });
|
||||
};
|
||||
|
||||
it('should init state and updater', () => {
|
||||
const { result } = setUp();
|
||||
const [sharedText, setSharedText] = result.current;
|
||||
|
||||
expect(sharedText).toEqual('init');
|
||||
expect(setSharedText).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should update the state', () => {
|
||||
const { result } = setUp();
|
||||
const [, setSharedText] = result.current;
|
||||
|
||||
act(() => setSharedText('changed'));
|
||||
|
||||
const [sharedText] = result.current;
|
||||
|
||||
expect(sharedText).toEqual('changed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using among multiple components', () => {
|
||||
const [useSharedText, SharedTextProvider] = createStateContext('init');
|
||||
|
||||
const DisplayComponent = () => {
|
||||
const [sharedText] = useSharedText();
|
||||
return <p>{sharedText}</p>;
|
||||
};
|
||||
|
||||
const UpdateComponent = () => {
|
||||
const [, setSharedText] = useSharedText();
|
||||
return (
|
||||
<button type="button" onClick={() => setSharedText('changed')}>
|
||||
UPDATE
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
it('should be in sync when under the same provider', () => {
|
||||
const { baseElement, getByText } = render(
|
||||
<SharedTextProvider>
|
||||
<DisplayComponent />
|
||||
<DisplayComponent />
|
||||
<UpdateComponent />
|
||||
</SharedTextProvider>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>init</p><p>init</p><button type="button">UPDATE</button></div>');
|
||||
|
||||
fireEvent.click(getByText('UPDATE'));
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>changed</p><p>changed</p><button type="button">UPDATE</button></div>');
|
||||
});
|
||||
|
||||
it('should be in update independently when under different providers', () => {
|
||||
const { baseElement, getByText } = render(
|
||||
<>
|
||||
<SharedTextProvider>
|
||||
<DisplayComponent />
|
||||
</SharedTextProvider>
|
||||
<SharedTextProvider>
|
||||
<DisplayComponent />
|
||||
<UpdateComponent />
|
||||
</SharedTextProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>init</p><p>init</p><button type="button">UPDATE</button></div>');
|
||||
|
||||
fireEvent.click(getByText('UPDATE'));
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>init</p><p>changed</p><button type="button">UPDATE</button></div>');
|
||||
});
|
||||
|
||||
it('should not update component that do not use the state context', () => {
|
||||
let renderCount = 0;
|
||||
const StaticComponent = () => {
|
||||
renderCount++;
|
||||
return <p>static</p>;
|
||||
};
|
||||
|
||||
const { baseElement, getByText } = render(
|
||||
<>
|
||||
<SharedTextProvider>
|
||||
<StaticComponent />
|
||||
<DisplayComponent />
|
||||
<UpdateComponent />
|
||||
</SharedTextProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>static</p><p>init</p><button type="button">UPDATE</button></div>');
|
||||
|
||||
fireEvent.click(getByText('UPDATE'));
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>static</p><p>changed</p><button type="button">UPDATE</button></div>');
|
||||
|
||||
expect(renderCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should override initialValue', () => {
|
||||
const { baseElement } = render(
|
||||
<>
|
||||
<SharedTextProvider>
|
||||
<DisplayComponent />
|
||||
</SharedTextProvider>
|
||||
<SharedTextProvider initialValue={'other'}>
|
||||
<DisplayComponent />
|
||||
</SharedTextProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
expect(baseElement.innerHTML).toBe('<div><p>init</p><p>other</p></div>');
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user