Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion shared/constants/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const useNav = () => {

export {wrapErrors} from '@/util/debug'
export {default as shallowEqual} from '@/util/shallow-equal'
export {useDebouncedCallback, useThrottledCallback, type DebouncedState} from 'use-debounce'
export {useDebouncedCallback, useThrottledCallback, type DebouncedState} from '@/util/use-debounce'
export {useShallow, useDeep} from '@/util/zustand'
export {default as useRPC} from '@/util/use-rpc'
export {produce} from 'immer'
Expand Down
15 changes: 12 additions & 3 deletions shared/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,18 @@ module.exports = {
setupFiles: ['<rootDir>/jest.setup.js'],
testEnvironment: 'node',
testMatch: [
'<rootDir>/stores/**/*.test.ts',
'<rootDir>/common-adapters/**/*.test.ts',
'<rootDir>/common-adapters/**/*.test.tsx',
'<rootDir>/**/*.test.ts',
'<rootDir>/**/*.test.tsx',
],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/desktop/dist/',
'<rootDir>/desktop/release/',
'<rootDir>/ios/',
'<rootDir>/android/',
'<rootDir>/images/',
'<rootDir>/perf/',
'<rootDir>/.tsOuts/',
],
Comment thread
chrisnojima marked this conversation as resolved.
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
Expand Down
1 change: 0 additions & 1 deletion shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@
"react-native-webview": "13.16.1",
"react-native-worklets": "0.8.1",
"react-native-zoom-toolkit": "5.0.1",
"use-debounce": "10.1.1",
"zustand": "5.0.12"
Comment thread
chrisnojima marked this conversation as resolved.
},
"devDependencies": {
Expand Down
289 changes: 289 additions & 0 deletions shared/util/use-debounce.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
/** @jest-environment jsdom */
/// <reference types="jest" />

import {afterEach, beforeEach, expect, jest, test} from '@jest/globals'
import {act, cleanup, renderHook} from '@testing-library/react'
import {useDebouncedCallback, useThrottledCallback} from './use-debounce'
Comment thread
chrisnojima marked this conversation as resolved.

const advance = (ms: number) => {
act(() => {
jest.advanceTimersByTime(ms)
})
}

beforeEach(() => {
jest.useFakeTimers()
})

afterEach(() => {
cleanup()
jest.restoreAllMocks()
jest.useRealTimers()
})

test('useDebouncedCallback delays calls until the trailing edge by default', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() => useDebouncedCallback(callback, 100))

let returnValue: string | undefined
act(() => {
returnValue = result.current('alpha')
})

expect(returnValue).toBeUndefined()
expect(callback).not.toHaveBeenCalled()

advance(99)
expect(callback).not.toHaveBeenCalled()

advance(1)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('alpha')
})

test('useDebouncedCallback with leading true does not fire an extra trailing call for a single invocation', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() => useDebouncedCallback(callback, 100, {leading: true}))

let returnValue: string | undefined
act(() => {
returnValue = result.current('alpha')
})

expect(returnValue).toBe('done:alpha')
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('alpha')

advance(100)
expect(callback).toHaveBeenCalledTimes(1)
expect(result.current.isPending()).toBe(false)
})

test('useDebouncedCallback supports combined leading and trailing behavior', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() =>
useDebouncedCallback(callback, 100, {leading: true, trailing: true})
)

act(() => {
result.current('alpha')
})
advance(50)
act(() => {
result.current('beta')
})

expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenNthCalledWith(1, 'alpha')

advance(99)
expect(callback).toHaveBeenCalledTimes(1)

advance(1)
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenNthCalledWith(2, 'beta')
})

test('useDebouncedCallback respects trailing false', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() =>
useDebouncedCallback(callback, 100, {leading: true, trailing: false})
)

act(() => {
result.current('alpha')
})
advance(50)
act(() => {
result.current('beta')
})

advance(100)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('alpha')

act(() => {
result.current('gamma')
})
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenNthCalledWith(2, 'gamma')
})

test('useDebouncedCallback cancel clears pending work', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() => useDebouncedCallback(callback, 100))

act(() => {
result.current('alpha')
})
expect(result.current.isPending()).toBe(true)

act(() => {
result.current.cancel()
})

expect(result.current.isPending()).toBe(false)
advance(100)
expect(callback).not.toHaveBeenCalled()
})

test('useDebouncedCallback flush runs pending work immediately', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() => useDebouncedCallback(callback, 100))

act(() => {
result.current('alpha')
})

let flushed: string | undefined
act(() => {
flushed = result.current.flush()
})

expect(flushed).toBe('done:alpha')
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('alpha')
expect(result.current.isPending()).toBe(false)
})

test('useDebouncedCallback uses the latest callback after rerender', () => {
const first = jest.fn((value: string) => `first:${value}`)
const second = jest.fn((value: string) => `second:${value}`)
const {result, rerender} = renderHook(
({callback}: {callback: (value: string) => string}) => useDebouncedCallback(callback, 100),
{initialProps: {callback: first}}
)

act(() => {
result.current('alpha')
})
rerender({callback: second})

advance(100)
expect(first).not.toHaveBeenCalled()
expect(second).toHaveBeenCalledTimes(1)
expect(second).toHaveBeenCalledWith('alpha')
})

test('useDebouncedCallback returns the last invocation result on later calls', () => {
const callback = jest.fn(() => 42)
const {result} = renderHook(() => useDebouncedCallback(callback, 100))

let firstReturn: number | undefined
act(() => {
firstReturn = result.current()
})
expect(firstReturn).toBeUndefined()

advance(100)
expect(callback).toHaveBeenCalledTimes(1)

let secondReturn: number | undefined
act(() => {
secondReturn = result.current()
})
expect(secondReturn).toBe(42)
})

test('useThrottledCallback defaults to leading and trailing calls without dropping cooldown after a trailing invoke', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() => useThrottledCallback(callback, 100))

let firstReturn: string | undefined
act(() => {
firstReturn = result.current('alpha')
})
expect(firstReturn).toBe('done:alpha')
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenNthCalledWith(1, 'alpha')

advance(50)
let secondReturn: string | undefined
act(() => {
secondReturn = result.current('beta')
})
expect(secondReturn).toBe('done:alpha')

advance(50)
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenNthCalledWith(2, 'beta')

advance(50)
act(() => {
result.current('gamma')
})
expect(callback).toHaveBeenCalledTimes(2)

advance(50)
expect(callback).toHaveBeenCalledTimes(3)
expect(callback).toHaveBeenNthCalledWith(3, 'gamma')
})

test('useThrottledCallback supports trailing false', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() =>
useThrottledCallback(callback, 100, {trailing: false})
)

act(() => {
result.current('alpha')
})
advance(50)
act(() => {
result.current('beta')
})

advance(50)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenNthCalledWith(1, 'alpha')

advance(50)
act(() => {
result.current('gamma')
})
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenNthCalledWith(2, 'gamma')
})

test('useThrottledCallback supports leading false', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() =>
useThrottledCallback(callback, 100, {leading: false, trailing: true})
)

act(() => {
result.current('alpha')
})
expect(callback).not.toHaveBeenCalled()

advance(50)
act(() => {
result.current('beta')
})
expect(callback).not.toHaveBeenCalled()

advance(50)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenNthCalledWith(1, 'beta')
})

test('useThrottledCallback collapses repeated calls within the wait window to the latest args', () => {
const callback = jest.fn((value: string) => `done:${value}`)
const {result} = renderHook(() => useThrottledCallback(callback, 100))

act(() => {
result.current('alpha')
})
advance(25)
act(() => {
result.current('beta')
})
advance(25)
act(() => {
result.current('gamma')
})

advance(50)
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenNthCalledWith(2, 'gamma')
})
Loading