Merge remote-tracking branch 'origin/master' into pr/857

This commit is contained in:
streamich 2020-02-15 14:13:19 +01:00
commit d0830b62e1
114 changed files with 5896 additions and 2327 deletions

View File

@ -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
View File

@ -0,0 +1,3 @@
{
"extends": "react-app"
}

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}

View File

@ -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)

View File

@ -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) &mdash; 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
View 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>
);
};
```

View 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 &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<button type="button" onClick={() => dispatch('increment')}>
+
</button>
</p>
);
};
const ComponentB = () => {
const [count, dispatch] = useSharedCounter();
return (
<p>
Component B &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<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();
// ...
}
```

View 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();
// ...
}
```

View File

@ -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
View 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);
```

View File

@ -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
View 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` &mdash; Callback of type `(err: Error) => void`

View File

@ -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` &mdash; `localStorage` key to manage.
- `initialValue` &mdash; initial value to set, if value in `localStorage` is empty.
- `raw` &mdash; boolean, if set to `true`, hook will not attempt to JSON serialize stored values.
- `serializer` &mdash; custom serializer (defaults to `JSON.stringify`)
- `deserializer` &mdash; custom deserializer (defaults to `JSON.parse`)

46
docs/useLongPress.md Normal file
View 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` &mdash; callback function.
- `options?` &mdash; optional parameter.
- `isPreventDefault?` &mdash; whether to call `event.preventDefault()` of `touchend` event, for preventing ghost click on mobile devices in some cases, defaults to `true`.
- `delay?` &mdash; delay in milliseconds after which to calls provided callback, defaults to `300`.

View File

@ -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
View 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>
);
};
```

View File

@ -46,7 +46,7 @@ If `stateSet` changed, became shorter than before and `currentIndex` left in shr
- **`state`**_`: T`_ &mdash; current state value;
- **`currentIndex`**_`: number`_ &mdash; current state index;
- **`prev()`**_`: void`_ &mdash; switches state to the previous one. If first element selected it will switch to the last one;
- **`nexct()`**_`: void`_ &mdash; switches state to the next one. If last element selected it will switch to the first one;
- **`next()`**_`: void`_ &mdash; switches state to the next one. If last element selected it will switch to the first one;
- **`setStateAt(newIndex: number)`**_`: void`_ &mdash; 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`_ &mdash; set the arbitrary state value that exists in `stateSet`. _In case new state does not exists in `stateSet` an Error will be thrown._

View File

@ -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"
]
}
}

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState, useMemo } from 'react';
const createBreakpoint = (

32
src/createGlobalState.ts Normal file
View 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;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useMemo } from 'react';
const createMemo = <T extends (...args: any) => any>(fn: T) => (...args: Parameters<T>) =>

View 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;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import * as React from 'react';
export interface RouterProviderProps {

23
src/createStateContext.ts Normal file
View 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;

View File

@ -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';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { DependencyList, useCallback, useState, useRef } from 'react';
import useMountedState from './useMountedState';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { DependencyList, useCallback, useState } from 'react';
import useAsync, { AsyncState } from './useAsync';

View File

@ -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) => {

View File

@ -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
View 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;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import writeText from 'copy-to-clipboard';
import { useCallback } from 'react';
import useMountedState from './useMountedState';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useMemo } from 'react';
import useGetSet from './useGetSet';
import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState';

View File

@ -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;

View File

@ -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;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import * as React from 'react';
import useMountedState from './useMountedState';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useMemo, useState } from 'react';
import useMountedState from './useMountedState';

19
src/useError.ts Normal file
View 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;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect } from 'react';
import { isClient } from './util';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { RefObject, useLayoutEffect, useState } from 'react';
import screenfull from 'screenfull';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
export interface GeoLocationSensorState {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { Dispatch, useMemo, useRef } from 'react';
import useUpdate from './useUpdate';
import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useCallback, useRef } from 'react';
import useUpdate from './useUpdate';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
// kudos: https://usehooks.com/

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
import { throttle } from 'throttle-debounce';
import { off, on } from './util';

View File

@ -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;
};

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { DependencyList, useMemo } from 'react';
import useEvent, { UseEventTarget } from './useEvent';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect } from 'react';
const useLifecycles = (mount, unmount?) => {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useMemo, useRef } from 'react';
import useUpdate from './useUpdate';
import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState';

View File

@ -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.

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
import { isClient, off, on } from './util';

View File

@ -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
View 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;

View File

@ -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
View 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;

View File

@ -34,4 +34,6 @@ const useMediaDevices = () => {
return state;
};
export default useMediaDevices;
const useMediaDevicesMock = () => ({});
export default typeof navigator === 'object' && !!navigator.mediaDevices ? useMediaDevices : useMediaDevicesMock;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react';
export interface StateMediator<S = any> {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useMemo, useReducer } from 'react';
const useMethods = (createMethods, initialState) => {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { RefObject, useEffect } from 'react';
import useRafState from './useRafState';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useCallback, useEffect, useRef, useState } from 'react';
import { StateValidator, UseStateValidatorReturn, ValidityState } from './useStateValidator';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
import { off, on } from './util';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
import { off, on } from './util';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect } from 'react';
const usePageLeave = (onPageLeave, args = []) => {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
import { off, on } from './util';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useCallback } from 'react';
import useMountedState from './useMountedState';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useRef, useState } from 'react';
export type RafLoopReturns = [() => void, boolean, () => void];

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { RefObject, useEffect } from 'react';
import useRafState from './useRafState';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { scrollbarWidth } from '@xobotyi/scrollbar-width';
import { useEffect, useState } from 'react';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { RefObject, useEffect, useState } from 'react';
const useScrolling = (ref: RefObject<HTMLElement>): boolean => {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useState, useEffect } from 'react';
const getValue = (search: string, param: string) => new URLSearchParams(search).get(param);

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useState } from 'react';
import { isClient } from './util';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useState, useMemo, useCallback } from 'react';
export interface StableActions<K> {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import * as React from 'react';
import { isClient } from './util';

142
src/useSlider.ts Normal file
View 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;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useMemo, useState } from 'react';
import { Spring, SpringSystem } from 'rebound';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useLayoutEffect } from 'react';
const isFocusedElementEditable = () => {

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useMemo, useRef } from 'react';
import useMountedState from './useMountedState';
import useUpdate from './useUpdate';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
export type ValidityState = [boolean | undefined, ...any[]];

View File

@ -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';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useRef, useState } from 'react';
import useUnmount from './useUnmount';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect, useRef, useState } from 'react';
import useUnmount from './useUnmount';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useCallback, useEffect, useRef } from 'react';
export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];

View File

@ -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) => {};

View File

@ -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);
};

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect } from 'react';
export type VibrationPattern = number | number[];

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect } from 'react';
import { isClient } from './util';

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { useEffect } from 'react';
import useRafState from './useRafState';

View File

@ -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');

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import * as React from 'react';
import { useEffect, useRef } from 'react';
import useSetState from '../useSetState';

View 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 />);

View 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 &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<button type="button" onClick={() => dispatch('increment')}>
+
</button>
</p>
);
};
const ComponentB = () => {
const [count, dispatch] = useSharedCounter();
return (
<p>
Component B &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<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 />);

View 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 />);

View File

@ -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 />);

View 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 />);

View File

@ -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 = () => {

View 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>
));

View 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 />);

View 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 />);

View 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);
});
});

View 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>');
});
});

View 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