diff --git a/README.md b/README.md index c31946d8..ca731051 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ - [`useDefault`](./docs/useDefault.md) — returns the default value when state is `null` or `undefined`. - [`useGetSet`](./docs/useGetSet.md) — returns state getter `get()` instead of raw state. - [`useGetSetState`](./docs/useGetSetState.md) — as if [`useGetSet`](./docs/useGetSet.md) and [`useSetState`](./docs/useSetState.md) had a baby. + - [`useLatest`](./docs/useLatest.md) — returns the latest state or props - [`usePrevious`](./docs/usePrevious.md) — returns the previous state or props. [![][img-demo]](https://codesandbox.io/s/fervent-galileo-krgx6) - [`usePreviousDistinct`](./docs/usePreviousDistinct.md) — like `usePrevious` but with a predicate to determine if `previous` should update. - [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`. diff --git a/docs/useLatest.md b/docs/useLatest.md new file mode 100644 index 00000000..cf1cc752 --- /dev/null +++ b/docs/useLatest.md @@ -0,0 +1,36 @@ +# `useLatest` + +React state hook that returns the latest state as described in the [React hooks FAQ](https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function). + +This is mostly useful to get access to the latest value of some props or state inside an asynchronous callback, instead of that value at the time the callback was created from. + +## Usage + +```jsx +import { useLatest } from 'react-use'; + +const Demo = () => { + const [count, setCount] = React.useState(0); + const latestCount = useLatest(count); + + function handleAlertClick() { + setTimeout(() => { + alert(`Latest count value: ${latestCount.current}`); + }, 3000); + } + + return ( +
+

You clicked {count} times

+ + +
+ ); +}; +``` + +## Reference + +```ts +const latestState = useLatest = (state: T): MutableRefObject; +``` diff --git a/src/index.ts b/src/index.ts index d632f7c9..8ef300d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ export { default as createBreakpoint } from './createBreakpoint'; // export { default as useKeyboardJs } from './useKeyboardJs'; export { default as useKeyPress } from './useKeyPress'; export { default as useKeyPressEvent } from './useKeyPressEvent'; +export { default as useLatest } from './useLatest'; export { default as useLifecycles } from './useLifecycles'; export { default as useList } from './useList'; export { default as useLocalStorage } from './useLocalStorage'; @@ -111,4 +112,4 @@ export { useRendersCount } from './useRendersCount'; export { useFirstMountState } from './useFirstMountState'; export { default as useSet } from './useSet'; export { createGlobalState } from './createGlobalState'; -export { useHash } from './useHash' \ No newline at end of file +export { useHash } from './useHash'; diff --git a/stories/useLatest.story.tsx b/stories/useLatest.story.tsx new file mode 100644 index 00000000..642e331c --- /dev/null +++ b/stories/useLatest.story.tsx @@ -0,0 +1,28 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useLatest } from '../src'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const [count, setCount] = React.useState(0); + const latestCount = useLatest(count); + const timeoutMs = 3000; + + function handleAlertClick() { + setTimeout(() => { + alert(`Latest count value: ${latestCount.current}`); + }, timeoutMs); + } + + return ( +
+

You clicked {count} times

+ + +
+ ); +}; + +storiesOf('State|useLatest', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/tests/useLatest.test.ts b/tests/useLatest.test.ts new file mode 100644 index 00000000..7bd1e0f9 --- /dev/null +++ b/tests/useLatest.test.ts @@ -0,0 +1,23 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useLatest from '../src/useLatest'; + +const setUp = () => renderHook(({ state }) => useLatest(state), { initialProps: { state: 0 } }); + +it('should return a ref with the latest value on initial render', () => { + const { result } = setUp(); + + expect(result.current).toEqual({ current: 0 }); +}); + +it('should always return a ref with the latest value after each update', () => { + const { result, rerender } = setUp(); + + rerender({ state: 2 }); + expect(result.current).toEqual({ current: 2 }); + + rerender({ state: 4 }); + expect(result.current).toEqual({ current: 4 }); + + rerender({ state: 6 }); + expect(result.current).toEqual({ current: 6 }); +});