diff --git a/README.md b/README.md index d7938ce6..7019a6a1 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ - [`useSpring`](./docs/useSpring.md) — interpolates number over time according to spring dynamics. - [`useTimeout`](./docs/useTimeout.md) — returns true after a timeout. - [`useTween`](./docs/useTween.md) — re-renders component, while tweening a number from 0 to 1. [![][img-demo]](https://codesandbox.io/s/52990wwzyl) + - [`useUpdate`](./docs/useUpdate.md) — returns a callback, which re-renders component when called.

- [**Side-effects**](./docs/Side-effects.md) @@ -71,6 +72,7 @@

- [**State**](./docs/State.md) + - [`useGetSet`](./docs/useGetSet.md) — returns state getter `get()` instead of raw state. - [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`. - [`useSetState`](./docs/useSetState.md) — creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0) - [`useToggle`](./docs/useToggle.md) — tracks state of a boolean. diff --git a/docs/useGetSet.md b/docs/useGetSet.md new file mode 100644 index 00000000..bda5cd40 --- /dev/null +++ b/docs/useGetSet.md @@ -0,0 +1,46 @@ +# `useGetSet` + +React state hook that returns state getter function instead of +raw state itself, this prevents subtle bugs when state is used +in nested functions. + + +## Usage + +Below example uses `useGetSet` to increment a number after 1 second +on each click. + +```jsx +import {useGetSet} from 'react-use'; + +const Demo = () => { + const [get, set] = useGetSet(0); + const onClick = () => { + setTimeout(() => { + set(get() + 1) + }, 1_000); + }; + + return ( + + ); +}; +``` + +If you would do this example in a naive way using regular `useState` +hook, the counter would not increment correctly if you click fast multiple times. + +```jsx +const DemoWrong = () => { + const [cnt, set] = useState(0); + const onClick = () => { + setTimeout(() => { + set(cnt + 1) + }, 1_000); + }; + + return ( + + ); +}; +``` diff --git a/docs/useUpdate.md b/docs/useUpdate.md new file mode 100644 index 00000000..9f9549ef --- /dev/null +++ b/docs/useUpdate.md @@ -0,0 +1,21 @@ +# `useUpdate` + +React utility hook that returns a function that forces component +to re-render when called. + + +## Usage + +```jsx +import {useUpdate} from 'react-use'; + +const Demo = () => { + const update = useUpdate(); + return ( + <> +
Time: {Date.now()}
+ + + ); +}; +``` diff --git a/src/__stories__/useGetSet.story.tsx b/src/__stories__/useGetSet.story.tsx new file mode 100644 index 00000000..540a4c9c --- /dev/null +++ b/src/__stories__/useGetSet.story.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import {storiesOf} from '@storybook/react'; +import {useGetSet} from '..'; +import {useState} from '../react'; +import ShowDocs from '../util/ShowDocs'; + +const Demo = () => { + const [get, set] = useGetSet(0); + const onClick = () => { + setTimeout(() => { + set(get() + 1) + }, 1_000); + }; + + return ( + + ); +}; + +const DemoWrong = () => { + const [cnt, set] = useState(0); + const onClick = () => { + setTimeout(() => { + set(cnt + 1) + }, 1_000); + }; + + return ( + + ); +}; + +storiesOf('useGetSet', module) + .add('Docs', () => ) + .add('Demo', () => + + ) + .add('DemoWrong', () => + + ) diff --git a/src/__stories__/useUpdate.story.tsx b/src/__stories__/useUpdate.story.tsx new file mode 100644 index 00000000..b445f3c2 --- /dev/null +++ b/src/__stories__/useUpdate.story.tsx @@ -0,0 +1,21 @@ +import {storiesOf} from '@storybook/react'; +import * as React from 'react'; +import {useUpdate} from '..'; +import ShowDocs from '../util/ShowDocs'; + +const Demo = () => { + const update = useUpdate(); + return ( + <> +
Time: {Date.now()}
+ + + ); +}; + +storiesOf('useUpdate', module) + .add('Docs', () => ) + .add('Demo', () => + + ) + diff --git a/src/index.ts b/src/index.ts index 3b63f020..974acbcb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import useCounter from './useCounter'; import useCss from './useCss'; import useFavicon from './useFavicon'; import useGeolocation from './useGeolocation'; +import useGetSet from './useGetSet'; import useHover from './useHover'; import useIdle from './useIdle'; import useLifecycles from './useLifecycles'; @@ -30,6 +31,7 @@ import useTitle from './useTitle'; import useToggle from './useToggle'; import useTween from './useTween'; import useUnmount from './useUnmount'; +import useUpdate from './useUpdate'; import useWindowSize from './useWindowSize'; export { @@ -40,6 +42,7 @@ export { useCss, useFavicon, useGeolocation, + useGetSet, useHover, useIdle, useLifecycles, @@ -65,5 +68,6 @@ export { useToggle, useTween, useUnmount, + useUpdate, useWindowSize, }; diff --git a/src/useGetSet.ts b/src/useGetSet.ts new file mode 100644 index 00000000..6e76bbb3 --- /dev/null +++ b/src/useGetSet.ts @@ -0,0 +1,17 @@ +import {useRef} from './react'; +import useUpdate from './useUpdate'; + +const useGetSet = (initialValue: T): [() => T, (value: T) => void] => { + const update = useUpdate(); + let state = useRef(initialValue); + + const get = () => state.current; + const set = (value: T) => { + state.current = value; + update(); + }; + + return [get, set]; +}; + +export default useGetSet; diff --git a/src/useUpdate.ts b/src/useUpdate.ts new file mode 100644 index 00000000..cb1948cb --- /dev/null +++ b/src/useUpdate.ts @@ -0,0 +1,5 @@ +import {useState} from './react'; + +const useUpdate = () => useState(0)[1] as (() => void); + +export default useUpdate;