Merge branch 'master' into useMediatedState

This commit is contained in:
Anton Zinovyev 2019-10-17 16:38:04 +03:00 committed by GitHub
commit 097fbf2c8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 592 additions and 90 deletions

View File

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

View File

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

View File

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

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

View File

@ -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",

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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,8 @@
"noImplicitAny": false,
"noFallthroughCasesInSwitch": true,
"outDir": "lib",
"lib": ["es2018", "dom"]
"lib": ["es2018", "dom"],
"importHelpers": true
},
"exclude": [
"node_modules",

View File

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