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

fix(react): improved useTimeout and added usePreservedCallback #966

Merged
merged 5 commits into from
Jun 24, 2024

Conversation

ssi02014
Copy link
Contributor

@ssi02014 ssi02014 commented Jun 24, 2024

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.

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

  • I did below actions if need
  1. I read the Contributing Guide
  2. I added documents and tests.

Copy link

changeset-bot bot commented Jun 24, 2024

🦋 Changeset detected

Latest commit: 4cb88a4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@suspensive/react Patch
@suspensive/react-query Patch

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

Copy link

vercel bot commented Jun 24, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 24, 2024 9:35am
v1.suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 24, 2024 9:35am
visualization.suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 24, 2024 9:35am

Copy link

codspeed-hq bot commented Jun 24, 2024

CodSpeed Performance Report

Merging #966 will create unknown performance changes

Comparing ssi02014:feat/usePreservedCallback (4cb88a4) with main (ec57398)

Summary

⚠️ No benchmarks were detected in both the base of the PR and the PR.

@codecov-commenter
Copy link

codecov-commenter commented Jun 24, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 80.88%. Comparing base (ec57398) to head (4cb88a4).

Additional details and impacted files

Impacted file tree graph

@@            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              
Components Coverage Δ
@suspensive/react 97.03% <100.00%> (+0.04%) ⬆️
@suspensive/react-query ∅ <ø> (∅)
@suspensive/react-query-4 0.00% <ø> (ø)
@suspensive/react-query-5 0.00% <ø> (ø)
@suspensive/react-await 100.00% <ø> (ø)
@suspensive/react-image 23.52% <ø> (ø)

Comment on lines +1 to +14
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
}
Copy link
Contributor Author

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.

Comment on lines +19 to +36
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)
})
Copy link
Contributor Author

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. 😭

@manudeli manudeli changed the title feat(react): improved useTimeout and added usePreservedCallback fix(react): improved useTimeout and added usePreservedCallback Jun 24, 2024
Copy link
Member

@manudeli manudeli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. @suspensive/test-utils should be changed like this implementation.
  2. 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)
Copy link
Member

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.

Copy link
Member

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'
}

Copy link
Contributor Author

@ssi02014 ssi02014 Jun 24, 2024

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)

Copy link
Member

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

@ssi02014
Copy link
Contributor Author

ssi02014 commented Jun 24, 2024

@manudeli

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

I did this because what was already written wasn't working as intended 🤔
If you don't want to guarantee latest function, the code below should be removed because it's pointless.

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 usePreservedCallback and want to guarantee that your function is up-to-date, you shouldn't put "fn" in the dependency array of useEffect because there is an infinite call issue.

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 ref.current directly without using useCallback as above, this is also problematic because it doesn't return the latest function.

Copy link
Member

@manudeli manudeli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better!

@manudeli manudeli merged commit 0051a0e into toss:main Jun 24, 2024
14 checks passed
@ssi02014 ssi02014 deleted the feat/usePreservedCallback branch June 24, 2024 09:37
@github-actions github-actions bot mentioned this pull request Jun 24, 2024
@ssi02014
Copy link
Contributor Author

ssi02014 commented Jun 24, 2024

@manudeli I recently took a deep dive into the usePreservedCallback hook.
As a result of this reflection, I'm happy to contribute to a great library once again. 🤗🚀

manudeli pushed a commit that referenced this pull request Jun 24, 2024
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>
manudeli added a commit that referenced this pull request Aug 3, 2024
# 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>
manudeli added a commit that referenced this pull request Aug 3, 2024
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>
manudeli pushed a commit that referenced this pull request Aug 3, 2024
# 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.

---------
manudeli added a commit that referenced this pull request Aug 3, 2024
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
manudeli added a commit that referenced this pull request Aug 3, 2024
# 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>
manudeli added a commit that referenced this pull request Aug 3, 2024
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
manudeli added a commit that referenced this pull request Aug 3, 2024
# 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>
manudeli added a commit that referenced this pull request Aug 3, 2024
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants