mirror of
https://github.com/streamich/react-use.git
synced 2025-12-08 18:02:14 +00:00
chore: 🤖 catch up with master
This commit is contained in:
commit
973a11fdb5
47
CHANGELOG.md
47
CHANGELOG.md
@ -1,3 +1,50 @@
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@ -95,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.
|
||||
@ -150,7 +152,6 @@
|
||||
- [**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 />
|
||||
|
||||
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);
|
||||
```
|
||||
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, remove] = useLocalStorage('my-key', 'foo');
|
||||
@ -22,15 +21,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`)
|
||||
|
||||
32
package.json
32
package.json
@ -46,9 +46,11 @@
|
||||
},
|
||||
"homepage": "https://github.com/streamich/react-use#readme",
|
||||
"dependencies": {
|
||||
"@types/js-cookie": "2.2.4",
|
||||
"@xobotyi/scrollbar-width": "1.5.0",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"fast-shallow-equal": "^1.0.0",
|
||||
"js-cookie": "^2.2.1",
|
||||
"nano-css": "^5.2.1",
|
||||
"react-fast-compare": "^2.0.4",
|
||||
"react-universal-interface": "^0.6.0",
|
||||
@ -64,22 +66,22 @@
|
||||
"react-dom": "^16.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.8.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.0",
|
||||
"@babel/preset-env": "7.8.0",
|
||||
"@babel/preset-react": "7.8.0",
|
||||
"@babel/preset-typescript": "7.8.0",
|
||||
"@babel/core": "7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.8.3",
|
||||
"@babel/preset-react": "7.8.3",
|
||||
"@babel/preset-typescript": "7.8.3",
|
||||
"@semantic-release/changelog": "3.0.6",
|
||||
"@semantic-release/git": "7.0.18",
|
||||
"@semantic-release/npm": "5.3.5",
|
||||
"@shopify/jest-dom-mocks": "2.8.8",
|
||||
"@storybook/addon-actions": "5.3.1",
|
||||
"@storybook/addon-knobs": "5.3.1",
|
||||
"@storybook/addon-notes": "5.3.1",
|
||||
"@storybook/addon-options": "5.3.1",
|
||||
"@storybook/react": "5.3.1",
|
||||
"@storybook/addon-actions": "5.3.5",
|
||||
"@storybook/addon-knobs": "5.3.5",
|
||||
"@storybook/addon-notes": "5.3.5",
|
||||
"@storybook/addon-options": "5.3.5",
|
||||
"@storybook/react": "5.3.5",
|
||||
"@testing-library/react-hooks": "3.2.1",
|
||||
"@types/jest": "24.0.25",
|
||||
"@types/jest": "24.9.0",
|
||||
"@types/react": "16.9.11",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-loader": "8.0.6",
|
||||
@ -88,6 +90,7 @@
|
||||
"gh-pages": "2.2.0",
|
||||
"husky": "3.1.0",
|
||||
"jest": "24.9.0",
|
||||
"jest-localstorage-mock": "2.4.0",
|
||||
"keyboardjs": "2.5.1",
|
||||
"lint-staged": "9.5.0",
|
||||
"markdown-loader": "5.1.0",
|
||||
@ -106,13 +109,13 @@
|
||||
"semantic-release": "15.14.0",
|
||||
"ts-jest": "24.3.0",
|
||||
"ts-loader": "6.2.1",
|
||||
"ts-node": "8.6.1",
|
||||
"ts-node": "8.6.2",
|
||||
"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"
|
||||
"typescript": "3.7.5"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
@ -160,7 +163,8 @@
|
||||
"<rootDir>/tests/**/*.test.(ts|tsx)"
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/tests/_setup.js"
|
||||
"<rootDir>/tests/_setup.js",
|
||||
"./tests/setupTests.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,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 +21,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';
|
||||
|
||||
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;
|
||||
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,8 +1,15 @@
|
||||
import { useEffect, useState, useCallback } 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 noop = () => {};
|
||||
const isUndefined = (value?: any): boolean => typeof value === 'undefined';
|
||||
@ -10,23 +17,25 @@ const isUndefined = (value?: any): boolean => typeof value === 'undefined';
|
||||
const useLocalStorage = <T>(
|
||||
key: string,
|
||||
initialValue?: T,
|
||||
raw?: boolean
|
||||
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => void] => {
|
||||
options?: parserOptions<T>
|
||||
): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>, () => void] => {
|
||||
if (!isClient) {
|
||||
return [initialValue as T, noop, noop];
|
||||
}
|
||||
|
||||
// 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 | undefined>(() => {
|
||||
try {
|
||||
const localStorageValue = localStorage.getItem(key);
|
||||
if (isUndefined(initialValue)) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof localStorageValue !== 'string') {
|
||||
localStorage.setItem(key, raw ? String(initialValue) : JSON.stringify(initialValue));
|
||||
if (localStorageValue !== null) {
|
||||
return deserializer(localStorageValue);
|
||||
} else {
|
||||
initialValue && localStorage.setItem(key, serializer(initialValue));
|
||||
return initialValue;
|
||||
}
|
||||
return raw ? localStorageValue : JSON.parse(localStorageValue || 'null');
|
||||
} catch {
|
||||
// If user is in private mode or has storage restriction
|
||||
// localStorage can throw. JSON.parse and JSON.stringify
|
||||
@ -48,8 +57,7 @@ const useLocalStorage = <T>(
|
||||
useEffect(() => {
|
||||
if (isUndefined(state)) return;
|
||||
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.
|
||||
|
||||
@ -15,15 +15,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 +58,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 +74,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 });
|
||||
}
|
||||
|
||||
@ -1,11 +1,22 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
function useTitle(title: string) {
|
||||
const t = useRef<string>();
|
||||
|
||||
if (t.current !== title) {
|
||||
document.title = t.current = title;
|
||||
}
|
||||
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) => {};
|
||||
|
||||
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 />);
|
||||
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/useLocalStorage.md')} />)
|
||||
.add('Demo', () => (
|
||||
<ErrorBoundary>
|
||||
<Demo />
|
||||
</ErrorBoundary>
|
||||
));
|
||||
1
tests/setupTests.ts
Normal file
1
tests/setupTests.ts
Normal file
@ -0,0 +1 @@
|
||||
import 'jest-localstorage-mock';
|
||||
@ -2,9 +2,6 @@ import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useCallback } from 'react';
|
||||
import useAsync from '../src/useAsync';
|
||||
|
||||
// NOTE: these tests cause console errors.
|
||||
// maybe we should test in a real environment instead
|
||||
// of a fake one?
|
||||
describe('useAsync', () => {
|
||||
it('should be defined', () => {
|
||||
expect(useAsync).toBeDefined();
|
||||
@ -34,8 +31,9 @@ describe('useAsync', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('initially starts loading', () => {
|
||||
it('initially starts loading', async () => {
|
||||
expect(hook.result.current.loading).toEqual(true);
|
||||
await hook.waitForNextUpdate();
|
||||
});
|
||||
|
||||
it('resolves', async () => {
|
||||
@ -75,8 +73,9 @@ describe('useAsync', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('initially starts loading', () => {
|
||||
it('initially starts loading', async () => {
|
||||
expect(hook.result.current.loading).toBeTruthy();
|
||||
await hook.waitForNextUpdate();
|
||||
});
|
||||
|
||||
it('resolves', async () => {
|
||||
|
||||
68
tests/useCookie.test.tsx
Normal file
68
tests/useCookie.test.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import Cookies from 'js-cookie';
|
||||
import { useCookie } from '../src';
|
||||
|
||||
const setup = (cookieName: string) => renderHook(() => useCookie(cookieName));
|
||||
|
||||
it('should have initial value of null if no cookie exists', () => {
|
||||
const { result } = setup('some-cookie');
|
||||
|
||||
expect(result.current[0]).toBeNull();
|
||||
});
|
||||
|
||||
it('should have initial value of the cookie if it exists', () => {
|
||||
const cookieName = 'some-cookie';
|
||||
const value = 'some-value';
|
||||
Cookies.set(cookieName, value);
|
||||
|
||||
const { result } = setup(cookieName);
|
||||
|
||||
expect(result.current[0]).toBe(value);
|
||||
|
||||
// cleanup
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
|
||||
it('should update the cookie on call to updateCookie', () => {
|
||||
const spy = jest.spyOn(Cookies, 'set');
|
||||
|
||||
const cookieName = 'some-cookie';
|
||||
const { result } = setup(cookieName);
|
||||
|
||||
const newValue = 'some-new-value';
|
||||
act(() => {
|
||||
result.current[1](newValue);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(newValue);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith(cookieName, newValue, undefined);
|
||||
|
||||
// cleanup
|
||||
spy.mockRestore();
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
|
||||
it('should delete the cookie on call to deleteCookie', () => {
|
||||
const cookieName = 'some-cookie';
|
||||
const value = 'some-value';
|
||||
Cookies.set(cookieName, value);
|
||||
|
||||
const spy = jest.spyOn(Cookies, 'remove');
|
||||
|
||||
const { result } = setup(cookieName);
|
||||
|
||||
expect(result.current[0]).toBe(value);
|
||||
|
||||
act(() => {
|
||||
result.current[2]();
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBeNull();
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenLastCalledWith(cookieName);
|
||||
|
||||
// cleanup
|
||||
spy.mockRestore();
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
26
tests/useError.test.ts
Normal file
26
tests/useError.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useError } from '../src';
|
||||
|
||||
const setup = () => renderHook(() => useError());
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw an error on error dispatch', () => {
|
||||
const errorStr = 'some_error';
|
||||
|
||||
try {
|
||||
const { result } = setup();
|
||||
|
||||
act(() => {
|
||||
result.current(new Error(errorStr));
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(errorStr);
|
||||
}
|
||||
});
|
||||
95
tests/useLocalStorage.test.ts
Normal file
95
tests/useLocalStorage.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useLocalStorage } from '../src';
|
||||
|
||||
const STRINGIFIED_VALUE = '{"a":"b"}';
|
||||
const JSONIFIED_VALUE = { a: 'b' };
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return undefined if no initialValue provided and localStorage empty', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('some_key'));
|
||||
|
||||
expect(result.current[0]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set the value from existing localStorage key', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key));
|
||||
|
||||
expect(result.current[0]).toEqual(JSONIFIED_VALUE);
|
||||
});
|
||||
|
||||
it('should return initialValue if localStorage empty and set that to localStorage', () => {
|
||||
const key = 'some_key';
|
||||
const value = 'some_value';
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, value));
|
||||
|
||||
expect(result.current[0]).toBe(value);
|
||||
expect(localStorage.__STORE__[key]).toBe(`"${value}"`);
|
||||
});
|
||||
|
||||
it('should return the value from localStorage if exists even if initialValue provied', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, 'random_value'));
|
||||
|
||||
expect(result.current[0]).toEqual(JSONIFIED_VALUE);
|
||||
});
|
||||
|
||||
it('should properly update the localStorage on change', () => {
|
||||
const key = 'some_key';
|
||||
const updatedValue = { b: 'a' };
|
||||
const expectedValue = '{"b":"a"}';
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key));
|
||||
|
||||
act(() => {
|
||||
result.current[1](updatedValue);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(updatedValue);
|
||||
expect(localStorage.__STORE__[key]).toBe(expectedValue);
|
||||
});
|
||||
|
||||
describe('Options with raw true', () => {
|
||||
it('should set the value from existing localStorage key', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, '', { raw: true }));
|
||||
|
||||
expect(result.current[0]).toEqual(STRINGIFIED_VALUE);
|
||||
});
|
||||
|
||||
it('should return initialValue if localStorage empty and set that to localStorage', () => {
|
||||
const key = 'some_key';
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage(key, STRINGIFIED_VALUE, { raw: true }));
|
||||
|
||||
expect(result.current[0]).toBe(STRINGIFIED_VALUE);
|
||||
expect(localStorage.__STORE__[key]).toBe(STRINGIFIED_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options with raw false and provided serializer/deserializer', () => {
|
||||
const serializer = (_: string) => '321';
|
||||
const deserializer = (_: string) => '123';
|
||||
|
||||
it('should return valid serialized value from existing localStorage key', () => {
|
||||
const key = 'some_key';
|
||||
localStorage.setItem(key, STRINGIFIED_VALUE);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage(key, STRINGIFIED_VALUE, { raw: false, serializer, deserializer })
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('123');
|
||||
});
|
||||
});
|
||||
@ -13,4 +13,16 @@ describe('useTitle', () => {
|
||||
hook.rerender('My other page title');
|
||||
expect(document.title).toBe('My other page title');
|
||||
});
|
||||
|
||||
it('should restore document title on unmount', () => {
|
||||
renderHook(props => useTitle(props), { initialProps: 'Old Title' });
|
||||
expect(document.title).toBe('Old Title');
|
||||
|
||||
const hook = renderHook(props => useTitle(props.title, { restoreOnUnmount: props.restore }), {
|
||||
initialProps: { title: 'New Title', restore: true },
|
||||
});
|
||||
expect(document.title).toBe('New Title');
|
||||
hook.unmount();
|
||||
expect(document.title).toBe('Old Title');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user