Skip to content

Commit

Permalink
Merge branch 'master' into useSlider
Browse files Browse the repository at this point in the history
  • Loading branch information
wardoost committed Oct 16, 2019
2 parents e657c2f + 109c6c4 commit 395e50a
Show file tree
Hide file tree
Showing 16 changed files with 301 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
refs:
container: &container
docker:
- image: node:12.11.1
- image: node:12.12.0
working_directory: ~/repo
steps:
- &Versions
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)


Expand Down
62 changes: 62 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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)
Expand Down
33 changes: 33 additions & 0 deletions docs/useRafState.md
Original file line number Diff line number Diff line change
@@ -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>;
};
```
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -76,7 +78,7 @@
"@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",
Expand Down
31 changes: 31 additions & 0 deletions src/__stories__/useRafState.story.tsx
Original file line number Diff line number Diff line change
@@ -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 />);
83 changes: 83 additions & 0 deletions src/__tests__/useRafState.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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
*/
Expand Down
46 changes: 21 additions & 25 deletions src/useMouse.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand All @@ -32,34 +33,29 @@ const useMouse = (ref: RefObject<Element>): State => {

useEffect(() => {
const moveHandler = (event: MouseEvent) => {
cancelAnimationFrame(frame.current);

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;
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]);
Expand Down
24 changes: 24 additions & 0 deletions src/useRafState.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 395e50a

Please sign in to comment.