mirror of
https://github.com/streamich/react-use.git
synced 2025-12-08 18:02:14 +00:00
Merge branch 'master' into useMediatedState
This commit is contained in:
commit
097fbf2c8e
@ -3,7 +3,7 @@ version: 2
|
||||
refs:
|
||||
container: &container
|
||||
docker:
|
||||
- image: node:12.9.1
|
||||
- image: node:12.12.0
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- &Versions
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
# [12.6.0](https://github.com/streamich/react-use/compare/v12.5.0...v12.6.0) (2019-10-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* useRafState ([#684](https://github.com/streamich/react-use/issues/684)) ([00816a4](https://github.com/streamich/react-use/commit/00816a4))
|
||||
|
||||
# [12.5.0](https://github.com/streamich/react-use/compare/v12.4.0...v12.5.0) (2019-10-13)
|
||||
|
||||
|
||||
|
||||
62
CONTRIBUTING.md
Normal file
62
CONTRIBUTING.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for being willing to contribute 🙌 If you contribute to this project, you agree to release your work under the license of this project.
|
||||
|
||||
**Working on your first Pull Request?** You can learn how from this [First Contributions](https://github.com/firstcontributions/first-contributions) guide.
|
||||
|
||||
## Project setup
|
||||
|
||||
1. Fork and clone the repo
|
||||
1. Run `yarn install` to install dependencies
|
||||
1. Create a branch for your PR with `git checkout -b pr/your-branch-name`
|
||||
|
||||
> Tip: Keep your `master` branch pointing at the original repository and make
|
||||
> pull requests from branches on your fork. To do this, run:
|
||||
>
|
||||
> ```sh
|
||||
> git remote add upstream https://github.com/streamich/react-use.git
|
||||
> git fetch upstream
|
||||
> git branch --set-upstream-to=upstream/master master
|
||||
> ```
|
||||
>
|
||||
> This will add the original repository as a "remote" called "upstream," Then
|
||||
> fetch the git information from that remote, then set your local `master`
|
||||
> branch to use the upstream master branch whenever you run `git pull`. Then you
|
||||
> can make all of your pull request branches based on this `master` branch.
|
||||
> Whenever you want to update your version of `master`, do a regular `git pull`.
|
||||
|
||||
## Development
|
||||
|
||||
This library is a collection of React hooks so a proposal for a new hook will need to utilize the [React Hooks API](https://reactjs.org/docs/hooks-reference.html) internally to be taken into consideration.
|
||||
|
||||
### Creating a new hook
|
||||
|
||||
1. Create `src/useYourHookName.ts` and `src/__stories__/useYourHookName.story.tsx`, run `yarn start` to start the storybook development server and start coding your hook
|
||||
1. Create `src/__tests__/useYourHookName.test.ts`, run `yarn test:watch` to start the test runner in watch mode and start writing tests for your hook
|
||||
1. Create `src/docs/useYourHookName.md` and create documentation for your hook
|
||||
1. Export your hook from `src/index.ts` and add your hook to `README.md`
|
||||
|
||||
You can also write your tests first if you prefer [test-driven development](https://en.wikipedia.org/wiki/Test-driven_development).
|
||||
|
||||
### Updating an existing hook
|
||||
|
||||
1. Run `yarn start` to start the storybook development server and start applying changes
|
||||
2. Update tests according to your changes using `yarn test:watch`
|
||||
3. Update documentation according to your changes
|
||||
|
||||
## Committing and Pushing changes
|
||||
|
||||
### Commit messages
|
||||
|
||||
This repo uses [semantic-release](https://github.com/semantic-release/semantic-release) and [conventional commit messages](https://conventionalcommits.org) so prefix your commits with `fix:` or `feat:` if you want your changes to appear in [release notes](https://github.com/streamich/react-use/blob/master/CHANGELOG.md).
|
||||
|
||||
### Git hooks
|
||||
|
||||
There are git hooks set up with this project that are automatically enabled
|
||||
when you install dependencies. These hooks automatically test and validate your code when creating commits. They're really handy but can be temporarily disabled by adding a `--no-verify` flag to your commit command. This is useful when you want to commit and push to get feedback on uncompleted code.
|
||||
|
||||
## Help needed
|
||||
|
||||
Please have a look at the [open issues](https://github.com/streamich/react-use/issues) and respond to questions, bug reports and feature requests. Thanks!
|
||||
|
||||
We're also looking to improve the code coverage on this project. To easily know what hooks need tests run `yarn test:coverage` to generate a code coverage report. You can see the report in your terminal or open `coverage/lcov-report/index.html` to see the HTML report.
|
||||
@ -128,6 +128,7 @@
|
||||
- [`useGetSetState`](./docs/useGetSetState.md) — as if [`useGetSet`](./docs/useGetSet.md) and [`useSetState`](./docs/useSetState.md) had a baby.
|
||||
- [`usePrevious`](./docs/usePrevious.md) — returns the previous state or props. [![][img-demo]](https://codesandbox.io/s/fervent-galileo-krgx6)
|
||||
- [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`.
|
||||
- [`useRafState`](./docs/useRafState.md) — creates `setState` method which only updates after `requestAnimationFrame`. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userafstate--demo)
|
||||
- [`useSetState`](./docs/useSetState.md) — creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0)
|
||||
- [`useStateList`](./docs/useStateList.md) — circularly iterates over an array. [![][img-demo]](https://codesandbox.io/s/bold-dewdney-pjzkd)
|
||||
- [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean. [![][img-demo]](https://codesandbox.io/s/focused-sammet-brw2d)
|
||||
@ -135,6 +136,7 @@
|
||||
- [`useList`](./docs/useList.md) and [`useUpsert`](./docs/useUpsert.md) — tracks state of an array. [![][img-demo]](https://codesandbox.io/s/wonderful-mahavira-1sm0w)
|
||||
- [`useMap`](./docs/useMap.md) — tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161)
|
||||
- [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo)
|
||||
- [`useMultiStateValidator`](./docs/useMultiStateValidator.md) — alike the `useStateValidator`, but tracks multiple states at a time. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemultistatevalidator--demo)
|
||||
- [`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)
|
||||
|
||||
<br />
|
||||
|
||||
55
docs/useMultiStateValidator.md
Normal file
55
docs/useMultiStateValidator.md
Normal file
@ -0,0 +1,55 @@
|
||||
# `useMultiStateValidator`
|
||||
|
||||
Each time any of given states changes - validator function is invoked.
|
||||
|
||||
## Usage
|
||||
```ts
|
||||
import * as React from 'react';
|
||||
import { useMultiStateValidator } from 'react-use';
|
||||
|
||||
const DemoStateValidator = (s: number[]) => [s.every((num: number) => !(num % 2))] as [boolean];
|
||||
const Demo = () => {
|
||||
const [state1, setState1] = React.useState<number>(1);
|
||||
const [state2, setState2] = React.useState<number>(1);
|
||||
const [state3, setState3] = React.useState<number>(1);
|
||||
const [[isValid]] = useMultiStateValidator([state1, state2, state3], DemoStateValidator);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Below fields will be valid if all of them is even</div>
|
||||
<input type="number" min="1" max="10" value={state1}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState1((ev.target.value as unknown) as number);
|
||||
}}
|
||||
/>
|
||||
<input type="number" min="1" max="10" value={state2}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState2((ev.target.value as unknown) as number);
|
||||
}}
|
||||
/>
|
||||
<input type="number" min="1" max="10" value={state3}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState3((ev.target.value as unknown) as number);
|
||||
}}
|
||||
/>
|
||||
{isValid !== null && <span>{isValid ? 'Valid!' : 'Invalid'}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Reference
|
||||
```ts
|
||||
const [validity, revalidate] = useStateValidator(
|
||||
state: any[] | { [p: string]: any } | { [p: number]: any },
|
||||
validator: (state, setValidity?)=>[boolean|null, ...any[]],
|
||||
initialValidity: any = [undefined]
|
||||
);
|
||||
```
|
||||
- **`state`**_`: any[] | { [p: string]: any } | { [p: number]: any }`_ can be both an array or object. It's _values_ will be used as a deps for inner `useEffect`.
|
||||
- **`validity`**_`: [boolean|null, ...any[]]`_ result of validity check. First element is strictly nullable boolean, but others can contain arbitrary data;
|
||||
- **`revalidate`**_`: ()=>void`_ runs validator once again
|
||||
- **`validator`**_`: (state, setValidity?)=>[boolean|null, ...any[]]`_ should return an array suitable for validity state described above;
|
||||
- `states` - current states values as the've been passed to the hook;
|
||||
- `setValidity` - if defined hook will not trigger validity change automatically. Useful for async validators;
|
||||
- `initialValidity` - validity value which set when validity is nt calculated yet;
|
||||
33
docs/useRafState.md
Normal file
33
docs/useRafState.md
Normal file
@ -0,0 +1,33 @@
|
||||
# `useRafState`
|
||||
|
||||
React state hook that only updates state in the callback of [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
import {useRafState, useMount} from 'react-use';
|
||||
|
||||
const Demo = () => {
|
||||
const [state, setState] = useRafState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
useMount(() => {
|
||||
const onResize = () => {
|
||||
setState({
|
||||
width: window.clientWidth,
|
||||
height: window.height,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
});
|
||||
|
||||
return <pre>{JSON.stringify(state, null, 2)}</pre>;
|
||||
};
|
||||
```
|
||||
12
package.json
12
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-use",
|
||||
"version": "12.5.0",
|
||||
"version": "12.6.0",
|
||||
"description": "Collection of React Hooks",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -15,6 +15,7 @@
|
||||
"start": "yarn storybook",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "tslint 'src/**/*.{ts,tsx}' -t verbose",
|
||||
"lint:fix": "yarn lint --fix",
|
||||
"lint:types": "tsc --noEmit",
|
||||
@ -54,7 +55,8 @@
|
||||
"screenfull": "^5.0.0",
|
||||
"set-harmonic-interval": "^1.0.0",
|
||||
"throttle-debounce": "^2.0.1",
|
||||
"ts-easing": "^0.2.0"
|
||||
"ts-easing": "^0.2.0",
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0",
|
||||
@ -69,19 +71,19 @@
|
||||
"@semantic-release/changelog": "3.0.4",
|
||||
"@semantic-release/git": "7.0.16",
|
||||
"@semantic-release/npm": "5.1.13",
|
||||
"@shopify/jest-dom-mocks": "2.8.2",
|
||||
"@shopify/jest-dom-mocks": "2.8.3",
|
||||
"@storybook/addon-actions": "5.1.11",
|
||||
"@storybook/addon-knobs": "5.1.11",
|
||||
"@storybook/addon-notes": "5.1.11",
|
||||
"@storybook/addon-options": "5.1.11",
|
||||
"@storybook/react": "5.1.11",
|
||||
"@testing-library/react-hooks": "2.0.3",
|
||||
"@types/jest": "24.0.18",
|
||||
"@types/jest": "24.0.19",
|
||||
"@types/react": "16.9.2",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-plugin-dynamic-import-node": "2.3.0",
|
||||
"fork-ts-checker-webpack-plugin": "1.5.0",
|
||||
"fork-ts-checker-webpack-plugin": "1.5.1",
|
||||
"gh-pages": "2.1.1",
|
||||
"husky": "3.0.9",
|
||||
"jest": "24.9.0",
|
||||
|
||||
51
src/__stories__/useMultiStateValidator.story.tsx
Normal file
51
src/__stories__/useMultiStateValidator.story.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import { useMultiStateValidator } from '../';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const DemoStateValidator = (s: number[]) => [s.every((num: number) => !(num % 2))] as [boolean];
|
||||
const Demo = () => {
|
||||
const [state1, setState1] = React.useState<number>(1);
|
||||
const [state2, setState2] = React.useState<number>(1);
|
||||
const [state3, setState3] = React.useState<number>(1);
|
||||
const [[isValid]] = useMultiStateValidator([state1, state2, state3], DemoStateValidator);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Below fields will be valid if all of them is even</div>
|
||||
<br />
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={state1}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState1((ev.target.value as unknown) as number);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={state2}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState2((ev.target.value as unknown) as number);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={state3}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState3((ev.target.value as unknown) as number);
|
||||
}}
|
||||
/>
|
||||
{isValid !== null && <span style={{ marginLeft: 24 }}>{isValid ? 'Valid!' : 'Invalid'}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf('State|useMultiStateValidator', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../../docs/useMultiStateValidator.md')} />)
|
||||
.add('Demo', () => <Demo />);
|
||||
31
src/__stories__/useRafState.story.tsx
Normal file
31
src/__stories__/useRafState.story.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
import { useRafState, useMount } from '..';
|
||||
import ShowDocs from './util/ShowDocs';
|
||||
|
||||
const Demo = () => {
|
||||
const [state, setState] = useRafState({ x: 0, y: 0 });
|
||||
|
||||
useMount(() => {
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
setState({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
const onTouchMove = (event: TouchEvent) => {
|
||||
setState({ x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY });
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('touchmove', onTouchMove);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
};
|
||||
});
|
||||
|
||||
return <pre>{JSON.stringify(state, null, 2)}</pre>;
|
||||
};
|
||||
|
||||
storiesOf('State|useRafState', module)
|
||||
.add('Docs', () => <ShowDocs md={require('../../docs/useRafState.md')} />)
|
||||
.add('Demo', () => <Demo />);
|
||||
125
src/__tests__/useMultiStateValidator.ts
Normal file
125
src/__tests__/useMultiStateValidator.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { useState } from 'react';
|
||||
import { MultiStateValidator, useMultiStateValidator } from '../useMultiStateValidator';
|
||||
import { UseValidatorReturn, ValidityState } from '../useStateValidator';
|
||||
|
||||
interface Mock extends jest.Mock {}
|
||||
|
||||
describe('useMultiStateValidator', () => {
|
||||
it('should be defined', () => {
|
||||
expect(useMultiStateValidator).toBeDefined();
|
||||
});
|
||||
|
||||
const defaultStatesValidator = (states: number[]) => [states.every(num => !(num % 2))];
|
||||
|
||||
function getHook(
|
||||
fn: MultiStateValidator<any, number[]> = jest.fn(defaultStatesValidator),
|
||||
initialStates = [1, 2],
|
||||
initialValidity = [false]
|
||||
): [MultiStateValidator<any, number[]>, RenderHookResult<any, [Function, UseValidatorReturn<ValidityState>]>] {
|
||||
return [
|
||||
fn,
|
||||
renderHook(
|
||||
({ initStates, validator, initValidity }) => {
|
||||
const [states, setStates] = useState(initStates);
|
||||
|
||||
return [setStates, useMultiStateValidator(states, validator, initValidity)];
|
||||
},
|
||||
{
|
||||
initialProps: {
|
||||
initStates: initialStates,
|
||||
initValidity: initialValidity,
|
||||
validator: fn,
|
||||
},
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
it('should return an array of two elements', () => {
|
||||
const [, hook] = getHook();
|
||||
const res = hook.result.current[1];
|
||||
|
||||
expect(Array.isArray(res)).toBe(true);
|
||||
expect(res.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should call validator on init', () => {
|
||||
const [spy] = getHook();
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call validator on any of states changed', () => {
|
||||
const [spy, hook] = getHook();
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
act(() => hook.result.current[0]([4, 2]));
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should NOT call validator on it's change", () => {
|
||||
const [spy, hook] = getHook();
|
||||
const newValidator: MultiStateValidator<any, number[]> = jest.fn(states => [states!.every(num => !!(num % 2))]);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
hook.rerender({ validator: newValidator });
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw if states is not an object', () => {
|
||||
expect(() => {
|
||||
// @ts-ignore
|
||||
const [, hook] = getHook(defaultStatesValidator, 123);
|
||||
|
||||
if (hook.result.error) {
|
||||
throw hook.result.error;
|
||||
}
|
||||
}).toThrowError('states expected to be an object or array, got number');
|
||||
});
|
||||
|
||||
it('first returned element should represent current validity state', () => {
|
||||
const [, hook] = getHook();
|
||||
let [setState, [validity]] = hook.result.current;
|
||||
expect(validity).toEqual([false]);
|
||||
|
||||
act(() => setState([4, 2]));
|
||||
[setState, [validity]] = hook.result.current;
|
||||
expect(validity).toEqual([true]);
|
||||
|
||||
act(() => setState([4, 5]));
|
||||
[setState, [validity]] = hook.result.current;
|
||||
expect(validity).toEqual([false]);
|
||||
});
|
||||
|
||||
it('second returned element should re-call validation', () => {
|
||||
const [spy, hook] = getHook();
|
||||
const [, [, revalidate]] = hook.result.current;
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
act(() => revalidate());
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('validator should receive states as a firs argument', () => {
|
||||
const [spy, hook] = getHook();
|
||||
const [setState] = hook.result.current;
|
||||
|
||||
expect((spy as Mock).mock.calls[0].length).toBe(1);
|
||||
expect((spy as Mock).mock.calls[0][0]).toEqual([1, 2]);
|
||||
act(() => setState([4, 6]));
|
||||
expect((spy as Mock).mock.calls[1][0]).toEqual([4, 6]);
|
||||
});
|
||||
|
||||
it('if validator expects 2nd parameters it should pass a validity setter there', () => {
|
||||
const spy = (jest.fn((states: number[], done) => {
|
||||
done([states.every(num => !!(num % 2))]);
|
||||
}) as unknown) as MultiStateValidator;
|
||||
const [, hook] = getHook(spy, [1, 3]);
|
||||
const [, [validity]] = hook.result.current;
|
||||
|
||||
expect((spy as Mock).mock.calls[0].length).toBe(2);
|
||||
expect((spy as Mock).mock.calls[0][0]).toEqual([1, 3]);
|
||||
expect(validity).toEqual([true]);
|
||||
});
|
||||
});
|
||||
83
src/__tests__/useRafState.test.ts
Normal file
83
src/__tests__/useRafState.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { replaceRaf } from 'raf-stub';
|
||||
import useRafState from '../useRafState';
|
||||
|
||||
interface RequestAnimationFrame {
|
||||
reset(): void;
|
||||
step(): void;
|
||||
}
|
||||
|
||||
declare var requestAnimationFrame: RequestAnimationFrame;
|
||||
|
||||
replaceRaf();
|
||||
|
||||
beforeEach(() => {
|
||||
requestAnimationFrame.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
requestAnimationFrame.reset();
|
||||
});
|
||||
|
||||
describe('useRafState', () => {
|
||||
it('should be defined', () => {
|
||||
expect(useRafState).toBeDefined();
|
||||
});
|
||||
|
||||
it('should only update state after requestAnimationFrame when providing an object', () => {
|
||||
const { result } = renderHook(() => useRafState(0));
|
||||
|
||||
act(() => {
|
||||
result.current[1](1);
|
||||
});
|
||||
expect(result.current[0]).toBe(0);
|
||||
|
||||
act(() => {
|
||||
requestAnimationFrame.step();
|
||||
});
|
||||
expect(result.current[0]).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current[1](2);
|
||||
requestAnimationFrame.step();
|
||||
});
|
||||
expect(result.current[0]).toBe(2);
|
||||
|
||||
act(() => {
|
||||
result.current[1](prevState => prevState * 2);
|
||||
requestAnimationFrame.step();
|
||||
});
|
||||
expect(result.current[0]).toBe(4);
|
||||
});
|
||||
|
||||
it('should only update state after requestAnimationFrame when providing a function', () => {
|
||||
const { result } = renderHook(() => useRafState(0));
|
||||
|
||||
act(() => {
|
||||
result.current[1](prevState => prevState + 1);
|
||||
});
|
||||
expect(result.current[0]).toBe(0);
|
||||
|
||||
act(() => {
|
||||
requestAnimationFrame.step();
|
||||
});
|
||||
expect(result.current[0]).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current[1](prevState => prevState * 3);
|
||||
requestAnimationFrame.step();
|
||||
});
|
||||
expect(result.current[0]).toBe(3);
|
||||
});
|
||||
|
||||
it('should cancel update state on unmount', () => {
|
||||
const { unmount } = renderHook(() => useRafState(0));
|
||||
const spyRafCancel = jest.spyOn(global, 'cancelAnimationFrame' as any);
|
||||
|
||||
expect(spyRafCancel).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
|
||||
expect(spyRafCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -61,6 +61,8 @@ export { default as usePreviousDistinct } from './usePreviousDistinct';
|
||||
export { default as usePromise } from './usePromise';
|
||||
export { default as useRaf } from './useRaf';
|
||||
export { default as useRafLoop } from './useRafLoop';
|
||||
export { default as useRafState } from './useRafState';
|
||||
|
||||
/**
|
||||
* @deprecated This hook is obsolete, use `useMountedState` instead
|
||||
*/
|
||||
@ -89,6 +91,7 @@ export { default as useUpdateEffect } from './useUpdateEffect';
|
||||
export { default as useUpsert } from './useUpsert';
|
||||
export { default as useVideo } from './useVideo';
|
||||
export { default as useStateValidator } from './useStateValidator';
|
||||
export { useMultiStateValidator } from './useMultiStateValidator';
|
||||
export { useWait, Waiter } from './useWait';
|
||||
export { default as useWindowScroll } from './useWindowScroll';
|
||||
export { default as useWindowSize } from './useWindowSize';
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
import useRafState from './useRafState';
|
||||
|
||||
export interface State {
|
||||
docX: number;
|
||||
@ -18,8 +20,7 @@ const useMouse = (ref: RefObject<Element>): State => {
|
||||
}
|
||||
}
|
||||
|
||||
const frame = useRef(0);
|
||||
const [state, setState] = useState<State>({
|
||||
const [state, setState] = useRafState<State>({
|
||||
docX: 0,
|
||||
docY: 0,
|
||||
posX: 0,
|
||||
@ -32,34 +33,29 @@ const useMouse = (ref: RefObject<Element>): State => {
|
||||
|
||||
useEffect(() => {
|
||||
const moveHandler = (event: MouseEvent) => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
if (ref && ref.current) {
|
||||
const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
|
||||
const posX = left + window.pageXOffset;
|
||||
const posY = top + window.pageYOffset;
|
||||
const elX = event.pageX - posX;
|
||||
const elY = event.pageY - posY;
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
if (ref && ref.current) {
|
||||
const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
|
||||
const posX = left + window.pageXOffset;
|
||||
const posY = top + window.pageYOffset;
|
||||
const elX = event.pageX - posX;
|
||||
const elY = event.pageY - posY;
|
||||
|
||||
setState({
|
||||
docX: event.pageX,
|
||||
docY: event.pageY,
|
||||
posX,
|
||||
posY,
|
||||
elX,
|
||||
elY,
|
||||
elH,
|
||||
elW,
|
||||
});
|
||||
}
|
||||
});
|
||||
setState({
|
||||
docX: event.pageX,
|
||||
docY: event.pageY,
|
||||
posX,
|
||||
posY,
|
||||
elX,
|
||||
elY,
|
||||
elH,
|
||||
elW,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', moveHandler);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
document.removeEventListener('mousemove', moveHandler);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
41
src/useMultiStateValidator.ts
Normal file
41
src/useMultiStateValidator.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { DispatchValidity, UseValidatorReturn, ValidityState } from './useStateValidator';
|
||||
|
||||
export type MultiStateValidatorStates = any[] | { [p: string]: any } | { [p: number]: any };
|
||||
|
||||
export interface MultiStateValidator<
|
||||
V extends ValidityState = ValidityState,
|
||||
S extends MultiStateValidatorStates = MultiStateValidatorStates
|
||||
> {
|
||||
(states: S): V;
|
||||
|
||||
(states: S, done: DispatchValidity<V>): void;
|
||||
}
|
||||
|
||||
export function useMultiStateValidator<
|
||||
V extends ValidityState = ValidityState,
|
||||
S extends MultiStateValidatorStates = MultiStateValidatorStates
|
||||
>(states: S, validator: MultiStateValidator<V, S>, initialValidity: V = [undefined] as V): UseValidatorReturn<V> {
|
||||
if (typeof states !== 'object') {
|
||||
throw new Error('states expected to be an object or array, got ' + typeof states);
|
||||
}
|
||||
|
||||
const validatorFn = useRef(validator);
|
||||
|
||||
const [validity, setValidity] = useState(initialValidity);
|
||||
|
||||
const deps = Array.isArray(states) ? states : Object.values(states);
|
||||
const validate = useCallback(() => {
|
||||
if (validatorFn.current.length === 2) {
|
||||
validatorFn.current(states, setValidity);
|
||||
} else {
|
||||
setValidity(validatorFn.current(states));
|
||||
}
|
||||
}, deps);
|
||||
|
||||
useEffect(() => {
|
||||
validate();
|
||||
}, deps);
|
||||
|
||||
return [validity, validate];
|
||||
}
|
||||
24
src/useRafState.ts
Normal file
24
src/useRafState.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useRef, useState, useCallback, Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import useUnmount from './useUnmount';
|
||||
|
||||
const useRafState = <S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] => {
|
||||
const frame = useRef(0);
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
setState(value);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useUnmount(() => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
});
|
||||
|
||||
return [state, setRafState];
|
||||
};
|
||||
|
||||
export default useRafState;
|
||||
@ -1,4 +1,6 @@
|
||||
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
import useRafState from './useRafState';
|
||||
|
||||
export interface State {
|
||||
x: number;
|
||||
@ -12,24 +14,19 @@ const useScroll = (ref: RefObject<HTMLElement>): State => {
|
||||
}
|
||||
}
|
||||
|
||||
const frame = useRef(0);
|
||||
const [state, setState] = useState<State>({
|
||||
const [state, setState] = useRafState<State>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
if (ref.current) {
|
||||
setState({
|
||||
x: ref.current.scrollLeft,
|
||||
y: ref.current.scrollTop,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (ref.current) {
|
||||
setState({
|
||||
x: ref.current.scrollLeft,
|
||||
y: ref.current.scrollTop,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (ref.current) {
|
||||
@ -40,10 +37,6 @@ const useScroll = (ref: RefObject<HTMLElement>): State => {
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (frame.current) {
|
||||
cancelAnimationFrame(frame.current);
|
||||
}
|
||||
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener('scroll', handler);
|
||||
}
|
||||
|
||||
@ -1,26 +1,24 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { isClient } from './util';
|
||||
|
||||
import useRafState from './useRafState';
|
||||
|
||||
export interface State {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const useWindowScroll = (): State => {
|
||||
const frame = useRef(0);
|
||||
const [state, setState] = useState<State>({
|
||||
const [state, setState] = useRafState<State>({
|
||||
x: isClient ? window.pageXOffset : 0,
|
||||
y: isClient ? window.pageYOffset : 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
setState({
|
||||
x: window.pageXOffset,
|
||||
y: window.pageYOffset,
|
||||
});
|
||||
setState({
|
||||
x: window.pageXOffset,
|
||||
y: window.pageYOffset,
|
||||
});
|
||||
};
|
||||
|
||||
@ -30,7 +28,6 @@ const useWindowScroll = (): State => {
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
window.removeEventListener('scroll', handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import useRafState from './useRafState';
|
||||
import { isClient } from './util';
|
||||
|
||||
const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => {
|
||||
const frame = useRef(0);
|
||||
const [state, setState] = useState<{ width: number; height: number }>({
|
||||
const [state, setState] = useRafState<{ width: number; height: number }>({
|
||||
width: isClient ? window.innerWidth : initialWidth,
|
||||
height: isClient ? window.innerHeight : initialHeight,
|
||||
});
|
||||
@ -11,21 +12,15 @@ const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => {
|
||||
useEffect(() => {
|
||||
if (isClient) {
|
||||
const handler = () => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
setState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
setState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handler);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
|
||||
window.removeEventListener('resize', handler);
|
||||
};
|
||||
} else {
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
"noImplicitAny": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"outDir": "lib",
|
||||
"lib": ["es2018", "dom"]
|
||||
"lib": ["es2018", "dom"],
|
||||
"importHelpers": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
39
yarn.lock
39
yarn.lock
@ -1712,10 +1712,10 @@
|
||||
into-stream "^4.0.0"
|
||||
lodash "^4.17.4"
|
||||
|
||||
"@shopify/async@^2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@shopify/async/-/async-2.0.7.tgz#944992bc1721df6c363b3f0f31be1dad0e75e929"
|
||||
integrity sha512-wYGjqPhpna4ShYbUmlD2fPv5ZkjNlCZtU7huUU8/snnyPmdgL/Rn5M5FPP6Apr7/hU5RgqMj2tJFs37ORz/VaQ==
|
||||
"@shopify/async@^2.0.8":
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@shopify/async/-/async-2.0.8.tgz#6ecded5fe8c5f6f20a74f212e4cd8f307e9e5138"
|
||||
integrity sha512-jiqVW8SA79eO3dWwJCCcLTch6TJK30i796WKyF1bGjIhwAaD6qRlspj5Ffzfg3r4RQbGMJheOVMdWhzctwxgKg==
|
||||
|
||||
"@shopify/decorators@^1.1.5":
|
||||
version "1.1.5"
|
||||
@ -1729,16 +1729,17 @@
|
||||
resolved "https://registry.yarnpkg.com/@shopify/function-enhancers/-/function-enhancers-1.0.5.tgz#7c3e516e26ce7a9b63c263679bdcf5121d994a10"
|
||||
integrity sha512-34ML8DX4RmmA9hXDlf2BAz4SA37unShZxoBRPz585a+FaEzNcMvw5NzLD+Ih9XrP/wrxTUcN+p6pazvoS+jB7w==
|
||||
|
||||
"@shopify/jest-dom-mocks@2.8.2":
|
||||
version "2.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@shopify/jest-dom-mocks/-/jest-dom-mocks-2.8.2.tgz#477c3159897807cc8d7797c33e8a79e787051779"
|
||||
integrity sha512-4drt+S1cQ1ZSP1DaEHAj5XPPCiI2R8IIt+ZnH9h08Ngy8PjtjFFNHNcSJ6bKBmk7eO2c6+5UaJQzNcg56nt7gg==
|
||||
"@shopify/jest-dom-mocks@2.8.3":
|
||||
version "2.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@shopify/jest-dom-mocks/-/jest-dom-mocks-2.8.3.tgz#3c00872c1b996290dc2a8222d2701c131150b305"
|
||||
integrity sha512-K544qSPkjlf/ze0urmgbN50ti4wcF+Vy1IQP8tbnkqA362slMXjdLokuB7oSIkntYLL9TmlEkWCxSRC17LXtnQ==
|
||||
dependencies:
|
||||
"@shopify/async" "^2.0.7"
|
||||
"@shopify/async" "^2.0.8"
|
||||
"@shopify/decorators" "^1.1.5"
|
||||
"@types/fetch-mock" "^6.0.1"
|
||||
"@types/lolex" "^2.1.3"
|
||||
fetch-mock "^6.3.0"
|
||||
lodash ">=4.0.0 <5.0.0"
|
||||
lolex "^2.7.5"
|
||||
promise "^8.0.3"
|
||||
tslib "^1.9.3"
|
||||
@ -2289,10 +2290,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89"
|
||||
integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==
|
||||
|
||||
"@types/jest@24.0.18":
|
||||
version "24.0.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.18.tgz#9c7858d450c59e2164a8a9df0905fc5091944498"
|
||||
integrity sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ==
|
||||
"@types/jest@24.0.19":
|
||||
version "24.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.19.tgz#f7036058d2a5844fe922609187c0ad8be430aff5"
|
||||
integrity sha512-YYiqfSjocv7lk5H/T+v5MjATYjaTMsUkbDnjGqSMoO88jWdtJXJV4ST/7DKZcoMHMBvB2SeSfyOzZfkxXHR5xg==
|
||||
dependencies:
|
||||
"@types/jest-diff" "*"
|
||||
|
||||
@ -5910,10 +5911,10 @@ fork-ts-checker-webpack-plugin@1.1.1:
|
||||
tapable "^1.0.0"
|
||||
worker-rpc "^0.1.0"
|
||||
|
||||
fork-ts-checker-webpack-plugin@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-1.5.0.tgz#ce1d77190b44d81a761b10b6284a373795e41f0c"
|
||||
integrity sha512-zEhg7Hz+KhZlBhILYpXy+Beu96gwvkROWJiTXOCyOOMMrdBIRPvsBpBqgTI4jfJGrJXcqGwJR8zsBGDmzY0jsA==
|
||||
fork-ts-checker-webpack-plugin@1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-1.5.1.tgz#f82d078ba8911c7b2c70703ffb3cbe588b33fbaa"
|
||||
integrity sha512-IbVh1Z46dmCXJMg6We8s9jYwCAzzSv2Tgj+G2Sg/8pFantHDBrAg/rQyPnmAWLS/djW7n4VEltoEglbtTvt0wQ==
|
||||
dependencies:
|
||||
babel-code-frame "^6.22.0"
|
||||
chalk "^2.4.1"
|
||||
@ -8336,7 +8337,7 @@ lodash.without@~4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
|
||||
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
|
||||
|
||||
lodash@>4.17.4, lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1:
|
||||
lodash@>4.17.4, "lodash@>=4.0.0 <5.0.0", lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
@ -12612,7 +12613,7 @@ tslib@1.9.0, tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"
|
||||
integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==
|
||||
|
||||
tslib@^1.9.3:
|
||||
tslib@^1.10.0, tslib@^1.9.3:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user