diff --git a/README.md b/README.md index d7938ce6b4..4647b9cdf3 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,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 0000000000..bda5cd40e1 --- /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/src/__stories__/useGetSet.story.tsx b/src/__stories__/useGetSet.story.tsx new file mode 100644 index 0000000000..540a4c9cd9 --- /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/index.ts b/src/index.ts index 3b63f02006..8c2e1dbbb0 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'; @@ -40,6 +41,7 @@ export { useCss, useFavicon, useGeolocation, + useGetSet, useHover, useIdle, useLifecycles, diff --git a/src/useGetSet.ts b/src/useGetSet.ts new file mode 100644 index 0000000000..2b1849cb6f --- /dev/null +++ b/src/useGetSet.ts @@ -0,0 +1,16 @@ +import {useState, useRef} from './react'; + +const useGetSet = (initialValue: T): [() => T, (value: T) => void] => { + const [_, update] = useState(undefined); + let state = useRef(initialValue); + + const get = () => state.current; + const set = (value: T) => { + state.current = value; + update(undefined); + }; + + return [get, set]; +}; + +export default useGetSet;