-
Notifications
You must be signed in to change notification settings - Fork 54
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
fix(react): improved useTimeout and added usePreservedCallback #966
Conversation
🦋 Changeset detectedLatest commit: 4cb88a4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
CodSpeed Performance ReportMerging #966 will create unknown performance changesComparing Summary
|
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #966 +/- ##
==========================================
+ Coverage 80.71% 80.88% +0.17%
==========================================
Files 37 38 +1
Lines 446 450 +4
Branches 99 99
==========================================
+ Hits 360 364 +4
Misses 77 77
Partials 9 9
|
import { useCallback, useRef } from 'react' | ||
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' | ||
|
||
export const usePreservedCallback = <T extends (...args: unknown[]) => unknown>(callback: T) => { | ||
const callbackRef = useRef<T>(callback) | ||
|
||
useIsomorphicLayoutEffect(() => { | ||
callbackRef.current = callback | ||
}, [callback]) | ||
|
||
return useCallback((...args: unknown[]) => { | ||
return callbackRef.current(...args) | ||
}, []) as T | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is slightly different from toss/slash's usePreservedCallback
.
I used useIsomorphicLayoutEffect
instead of useEffect
, and changed the type to unknown
instead of any
.
f344eab
to
0ac379d
Compare
it('should call the modified function when the given function is changed', async () => { | ||
const timeoutMockFn1 = vi.fn() | ||
const timeoutMockFn2 = vi.fn() | ||
|
||
const { rerender } = renderHook(({ fn }) => useTimeout(fn, ms('0.1s')), { | ||
initialProps: { fn: timeoutMockFn1 }, | ||
}) | ||
|
||
rerender({ fn: timeoutMockFn2 }) | ||
|
||
expect(timeoutMockFn1).toHaveBeenCalledTimes(0) | ||
expect(timeoutMockFn2).toHaveBeenCalledTimes(0) | ||
|
||
await sleep(ms('0.1s')) | ||
|
||
expect(timeoutMockFn1).toHaveBeenCalledTimes(0) | ||
expect(timeoutMockFn2).toHaveBeenCalledTimes(1) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's existing code, it won't call the changed function as intended, and the test will fail. 😭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- @suspensive/test-utils should be changed like this implementation.
- To make how to work be same between AS-IS and TO-BE, we should add useCallback in all components using useTimeout. There is no significant difference in components that use useTimeout, but it can be expected that performance will be slightly worse. So I'm confuse that this is really improvement. I need more reasons to merge this change
useIsomorphicLayoutEffect(() => { | ||
fnRef.current = fn | ||
}, [fn]) | ||
const preservedCallback = usePreservedCallback(fn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Components(Delay) using useTimeout should use useCallback too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ssi02014 I absolutely accept your opinion #966 (comment) but anyway
I think Components(Delay) using useTimeout should use useCallback too. doesn't it right?
TO-BE
import { type PropsWithChildren, type ReactNode, useContext, useState } from 'react'
import { DelayDefaultPropsContext } from './contexts'
import { useTimeout } from './hooks'
import { Message_Delay_ms_prop_should_be_greater_than_or_equal_to_0, SuspensiveError } from './models/SuspensiveError'
export interface DelayProps extends PropsWithChildren {
ms?: number
fallback?: ReactNode
}
export const Delay = (props: DelayProps) => {
if (process.env.NODE_ENV === 'development') {
if (typeof props.ms === 'number') {
SuspensiveError.assert(props.ms >= 0, Message_Delay_ms_prop_should_be_greater_than_or_equal_to_0)
}
}
const defaultProps = useContext(DelayDefaultPropsContext)
const ms = props.ms ?? defaultProps.ms ?? 0
const [isDelaying, setIsDelaying] = useState(ms > 0)
- useTimeout(() => setIsDelaying(false), ms)
+ useTimeout(useCallback(() => setIsDelaying(false), []), ms)
const fallback = typeof props.fallback === 'undefined' ? defaultProps.fallback : props.fallback
return <>{isDelaying ? fallback : props.children}</>
}
if (process.env.NODE_ENV === 'development') {
Delay.displayName = 'Delay'
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@manudeli Oh no, useTimeout
works the same way.
No need to change it! 👍
In usePreservedCallback
, the callback
is wrapped in useCallback
, so there is no difference in usage.
useTimeout(() => setIsDelaying(false), ms)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! I understood what you mean! Thanks
I did this because what was already written wasn't working as intended 🤔 import { useEffect, useRef } from 'react'
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
export const useTimeout = (fn: () => void, ms: number) => {
- const fnRef = useRef(fn)
- useIsomorphicLayoutEffect(() => {
- fnRef.current = fn
- }, [fn])
useEffect(() => {
- const id = setTimeout(fnRef.current, ms)
+ const id = setTimeout(fn, ms)
return () => clearTimeout(id)
}, [ms])
} ++ Even if you don't use export const usePreservedCallback = <T extends (...args: unknown[]) => unknown>(callback: T) => {
const callbackRef = useRef<T>(callback)
useIsomorphicLayoutEffect(() => {
callbackRef.current = callback
}, [callback])
return callbackRef.current // (*)
} ++ If you return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much better!
@manudeli I recently took a deep dive into the |
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @suspensive/react@2.2.2 ### Patch Changes - [#966](#966) [`0051a0e`](0051a0e) Thanks [@ssi02014](https://github.com/ssi02014)! - fix(react): improved useTimeout and added usePreservedCallback ## @suspensive/react-query@2.2.2 ### Patch Changes - Updated dependencies \[[`0051a0e`](0051a0e)]: - @suspensive/react@2.2.2 - @suspensive/react-query-4@0.0.1 - @suspensive/react-query-5@0.0.1 Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
# Overview @manudeli Hello! 👋 Currently, `useTimeout` hook is written to call the changed function when the callback function changes. However, in practice, we're not actually calling the changed function, even if the function changes in the middle. This is because the only value we currently depend on for `useEffect` is `ms`. Adding `ref` to the dependency array doesn't solve this problem because it's not tracked. These problems can be solved by using hooks like `usePreservedCallback` in `toss/slash`, which can preserve the reference of the callback function, while at the same time ensuring the function is up-to-date. - [usePreservedCallback](https://github.com/toss/slash/blob/main/packages/react/react/src/hooks/usePreservedCallback.ts) Since the `ref` keeps updating during its lifecycle, when calling the function returned by `useCallback`, the latest function will be called by the `closure`. ## PR Checklist - [x] I did below actions if need 1. I read the [Contributing Guide](https://github.com/toss/suspensive/blob/main/CONTRIBUTING.md) 2. I added documents and tests. --------- Co-authored-by: Jonghyeon Ko <jonghyeon@toss.im>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @suspensive/react@2.2.2 ### Patch Changes - [#966](#966) [`0051a0e`](0051a0e) Thanks [@ssi02014](https://github.com/ssi02014)! - fix(react): improved useTimeout and added usePreservedCallback ## @suspensive/react-query@2.2.2 ### Patch Changes - Updated dependencies \[[`0051a0e`](0051a0e)]: - @suspensive/react@2.2.2 - @suspensive/react-query-4@0.0.1 - @suspensive/react-query-5@0.0.1 Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
# Overview @manudeli Hello! 👋 Currently, `useTimeout` hook is written to call the changed function when the callback function changes. However, in practice, we're not actually calling the changed function, even if the function changes in the middle. This is because the only value we currently depend on for `useEffect` is `ms`. Adding `ref` to the dependency array doesn't solve this problem because it's not tracked. These problems can be solved by using hooks like `usePreservedCallback` in `toss/slash`, which can preserve the reference of the callback function, while at the same time ensuring the function is up-to-date. - [usePreservedCallback](https://github.com/toss/slash/blob/main/packages/react/react/src/hooks/usePreservedCallback.ts) Since the `ref` keeps updating during its lifecycle, when calling the function returned by `useCallback`, the latest function will be called by the `closure`. ## PR Checklist - [x] I did below actions if need 1. I read the [Contributing Guide](https://github.com/toss/suspensive/blob/main/CONTRIBUTING.md) 2. I added documents and tests. ---------
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @suspensive/react@2.2.2 ### Patch Changes - [#966](#966) [`0051a0e`](0051a0e) Thanks [@ssi02014](https://github.com/ssi02014)! - fix(react): improved useTimeout and added usePreservedCallback ## @suspensive/react-query@2.2.2 ### Patch Changes - Updated dependencies \[[`0051a0e`](0051a0e)]: - @suspensive/react@2.2.2 - @suspensive/react-query-4@0.0.1 - @suspensive/react-query-5@0.0.1
# Overview @manudeli Hello! 👋 Currently, `useTimeout` hook is written to call the changed function when the callback function changes. However, in practice, we're not actually calling the changed function, even if the function changes in the middle. This is because the only value we currently depend on for `useEffect` is `ms`. Adding `ref` to the dependency array doesn't solve this problem because it's not tracked. These problems can be solved by using hooks like `usePreservedCallback` in `toss/slash`, which can preserve the reference of the callback function, while at the same time ensuring the function is up-to-date. - [usePreservedCallback](https://github.com/toss/slash/blob/main/packages/react/react/src/hooks/usePreservedCallback.ts) Since the `ref` keeps updating during its lifecycle, when calling the function returned by `useCallback`, the latest function will be called by the `closure`. ## PR Checklist - [x] I did below actions if need 1. I read the [Contributing Guide](https://github.com/toss/suspensive/blob/main/CONTRIBUTING.md) 2. I added documents and tests. --------- Co-authored-by: Jonghyeon Ko <jonghyeon@toss.im>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @suspensive/react@2.2.2 ### Patch Changes - [#966](#966) [`0051a0e`](0051a0e) Thanks [@ssi02014](https://github.com/ssi02014)! - fix(react): improved useTimeout and added usePreservedCallback ## @suspensive/react-query@2.2.2 ### Patch Changes - Updated dependencies \[[`0051a0e`](0051a0e)]: - @suspensive/react@2.2.2 - @suspensive/react-query-4@0.0.1 - @suspensive/react-query-5@0.0.1
# Overview @manudeli Hello! 👋 Currently, `useTimeout` hook is written to call the changed function when the callback function changes. However, in practice, we're not actually calling the changed function, even if the function changes in the middle. This is because the only value we currently depend on for `useEffect` is `ms`. Adding `ref` to the dependency array doesn't solve this problem because it's not tracked. These problems can be solved by using hooks like `usePreservedCallback` in `toss/slash`, which can preserve the reference of the callback function, while at the same time ensuring the function is up-to-date. - [usePreservedCallback](https://github.com/toss/slash/blob/main/packages/react/react/src/hooks/usePreservedCallback.ts) Since the `ref` keeps updating during its lifecycle, when calling the function returned by `useCallback`, the latest function will be called by the `closure`. ## PR Checklist - [x] I did below actions if need 1. I read the [Contributing Guide](https://github.com/toss/suspensive/blob/main/CONTRIBUTING.md) 2. I added documents and tests. --------- Co-authored-by: Jonghyeon Ko <jonghyeon@toss.im>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @suspensive/react@2.2.2 ### Patch Changes - [#966](#966) [`0051a0e`](0051a0e) Thanks [@ssi02014](https://github.com/ssi02014)! - fix(react): improved useTimeout and added usePreservedCallback ## @suspensive/react-query@2.2.2 ### Patch Changes - Updated dependencies \[[`0051a0e`](0051a0e)]: - @suspensive/react@2.2.2 - @suspensive/react-query-4@0.0.1 - @suspensive/react-query-5@0.0.1
Overview
@manudeli Hello! 👋
Currently,
useTimeout
hook is written to call the changed function when the callback function changes.However, in practice, we're not actually calling the changed function, even if the function changes in the middle.
This is because the only value we currently depend on for
useEffect
isms
.Adding
ref
to the dependency array doesn't solve this problem because it's not tracked.These problems can be solved by using hooks like
usePreservedCallback
intoss/slash
, which can preserve the reference of the callback function, while at the same time ensuring the function is up-to-date.Since the
ref
keeps updating during its lifecycle, when calling the function returned byuseCallback
, the latest function will be called by theclosure
.PR Checklist