From d6fe2676153f19302ce170b03ceadc3bab5a945a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Paris?= Date: Sun, 10 May 2020 17:57:13 +0200 Subject: [PATCH 1/3] feat: add useLatest hook --- README.md | 1 + docs/useLatest.md | 36 ++++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++- src/useLatest.ts | 13 +++++++++++++ stories/useLatest.story.tsx | 27 +++++++++++++++++++++++++++ tests/useLatest.test.ts | 23 +++++++++++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 docs/useLatest.md create mode 100644 src/useLatest.ts create mode 100644 stories/useLatest.story.tsx create mode 100644 tests/useLatest.test.ts diff --git a/README.md b/README.md index 3f9d1250..f8cf85f9 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 6d018164..8a91022e 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'; @@ -110,4 +111,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/src/useLatest.ts b/src/useLatest.ts new file mode 100644 index 00000000..fa82887c --- /dev/null +++ b/src/useLatest.ts @@ -0,0 +1,13 @@ +import { useRef, useEffect, MutableRefObject } from 'react'; + +const useLatest = (value: T): MutableRefObject => { + const latest = useRef(value); + + useEffect(() => { + latest.current = value; + }, [value]); + + return latest; +}; + +export default useLatest; diff --git a/stories/useLatest.story.tsx b/stories/useLatest.story.tsx new file mode 100644 index 00000000..6c2969b5 --- /dev/null +++ b/stories/useLatest.story.tsx @@ -0,0 +1,27 @@ +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); + + function handleAlertClick() { + setTimeout(() => { + alert(`Latest count value: ${latestCount.current}`); + }, 3000); + } + + 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 }); +}); From 5488f5eb3e8504dcae03584b5797a48659e16623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Paris?= Date: Wed, 13 May 2020 19:06:45 +0200 Subject: [PATCH 2/3] fix: mutate ref in render directly --- src/useLatest.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/useLatest.ts b/src/useLatest.ts index fa82887c..17b8c967 100644 --- a/src/useLatest.ts +++ b/src/useLatest.ts @@ -1,11 +1,9 @@ -import { useRef, useEffect, MutableRefObject } from 'react'; +import { useRef, MutableRefObject } from 'react'; const useLatest = (value: T): MutableRefObject => { const latest = useRef(value); - useEffect(() => { - latest.current = value; - }, [value]); + latest.current = value; return latest; }; From 2bb65ef3d85e82b6bd134a714e51e27876037734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Paris?= Date: Wed, 13 May 2020 19:07:33 +0200 Subject: [PATCH 3/3] fix: display alert timeout in story --- stories/useLatest.story.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stories/useLatest.story.tsx b/stories/useLatest.story.tsx index 6c2969b5..642e331c 100644 --- a/stories/useLatest.story.tsx +++ b/stories/useLatest.story.tsx @@ -6,18 +6,19 @@ 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}`); - }, 3000); + }, timeoutMs); } return (

You clicked {count} times

- +
); };