Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useLatestRef() and useLayoutLatestRef() #2509

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/useLatest.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ React state hook that returns the latest state as described in the [React hooks

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.

Note: This hook updates the ref value during rendering, and is therefore potentially unsafe. Use `useLatestRef()` if you want to safely access the latest value. For more information, see "Pitfall" section of [React `useRef()` docs](https://react.dev/reference/react/useRef).

## Usage

```jsx
Expand Down
40 changes: 40 additions & 0 deletions docs/useLatestRef.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# `useLatestRef`

React state hook that stores the latest version of a value.

This can be used to access the latest ("fresh") value of some props or state inside an event handler or `useEffect()` without specifying the value in a dependency array.

Since this hook updates the ref with a `useEffect()`, the ref value may be outdated ("stale") when accessed in a `useLayoutEffect()` or during rendering. If you need to access the latest value in a `useLayoutEffect()`, use `useLayoutLatestRef()` instead. Do not access the ref value during rendering as it is [considered a bad practice](https://react.dev/learn/referencing-values-with-refs#best-practices-for-refs).

This is similar to `useLatest()` but safe from the concurrency issues that `useLatest()` may suffer from. For more information, see "Pitfall" section of [React `useRef()` docs](https://react.dev/reference/react/useRef).

## Usage

```jsx
import { useLatestRef } from "react-use";

const Demo = () => {
const [count, setCount] = React.useState(0);
const latestCount = useLatestRef(count);

function handleAlertClick() {
setTimeout(() => {
alert(`Latest count value: ${latestCount.current}`);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
};
```

## Reference

```ts
const latestRef = useLatestRef(someValue);
```
38 changes: 38 additions & 0 deletions docs/useLayoutLatestRef.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `useLayoutLatestRef`

React state hook that stores the latest version of a value.

This can be used to access the latest ("fresh") value of some props or state inside an event handler or `useLayoutEffect()` without specifying the value in a dependency array.

This hook is similar to `useLatestRef()` except that it provide the latest value even when the ref is accessed in a `useLayoutEffect()`.

## Usage

```jsx
import { useLayoutLatestRef } from "react-use";

const Demo = () => {
const [count, setCount] = React.useState(0);
const latestCount = useLayoutLatestRef(count);

function handleAlertClick() {
setTimeout(() => {
alert(`Latest count value: ${latestCount.current}`);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
};
```

## Reference

```ts
const latestRef = useLayoutLatestRef(someValue);
```
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export { default as createBreakpoint } from './factory/createBreakpoint';
export { default as useKeyPress } from './useKeyPress';
export { default as useKeyPressEvent } from './useKeyPressEvent';
export { default as useLatest } from './useLatest';
export { default as useLatestRef } from './useLatestRef';
export { default as useLayoutLatestRef } from './useLayoutLatestRef';
export { default as useLifecycles } from './useLifecycles';
export { default as useList } from './useList';
export { default as useLocalStorage } from './useLocalStorage';
Expand Down
3 changes: 3 additions & 0 deletions src/useLatest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useRef } from 'react';

/**
* @deprecated Use `useLatestRef` instead
*/
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
Expand Down
11 changes: 11 additions & 0 deletions src/useLatestRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';

const useLatestRef = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
});
return ref;
};

export default useLatestRef;
11 changes: 11 additions & 0 deletions src/useLayoutLatestRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useLayoutEffect, useRef } from 'react';

const useLatestRef = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
useLayoutEffect(() => {
ref.current = value;
});
return ref;
};

export default useLatestRef;
28 changes: 28 additions & 0 deletions stories/useLatestRef.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useLatestRef } from '../src';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const [count, setCount] = React.useState(0);
const latestCount = useLatestRef(count);
const timeoutMs = 3000;

function handleAlertClick() {
setTimeout(() => {
alert(`Latest count value: ${latestCount.current}`);
}, timeoutMs);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert in {timeoutMs / 1000}s</button>
</div>
);
};

storiesOf('State/useLatestRef', module)
.add('Docs', () => <ShowDocs md={require('../docs/useLatestRef.md')} />)
.add('Demo', () => <Demo />);
28 changes: 28 additions & 0 deletions stories/useLayoutLatestRef.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useLayoutLatestRef } from '../src';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const [count, setCount] = React.useState(0);
const latestCount = useLayoutLatestRef(count);
const timeoutMs = 3000;

function handleAlertClick() {
setTimeout(() => {
alert(`Latest count value: ${latestCount.current}`);
}, timeoutMs);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert in {timeoutMs / 1000}s</button>
</div>
);
};

storiesOf('State/useLayoutLatestRef', module)
.add('Docs', () => <ShowDocs md={require('../docs/useLayoutLatestRef.md')} />)
.add('Demo', () => <Demo />);
23 changes: 23 additions & 0 deletions tests/useLatestRef.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { renderHook } from '@testing-library/react-hooks';
import useLatestRef from '../src/useLatestRef';

const setUp = () => renderHook(({ state }) => useLatestRef(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 });
});
24 changes: 24 additions & 0 deletions tests/useLayoutLatestRef.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { renderHook } from '@testing-library/react-hooks';
import useLayoutLatestRef from '../src/useLayoutLatestRef';

const setUp = () =>
renderHook(({ state }) => useLayoutLatestRef(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 });
});