Created
July 18, 2025 17:08
-
-
Save levchenkod/3c7e3d8dc0e2d34747bd2b8848839c38 to your computer and use it in GitHub Desktop.
useResetFormAsync - React Hook Form async reset
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | |
import { render, act } from '#tests/utils'; | |
import { FormProvider, useForm } from 'react-hook-form'; | |
import { useResetFormAsync } from './useResetFormAsync'; | |
import React, { useEffect } from 'react'; | |
vi.useFakeTimers(); | |
// Mock requestAnimationFrame to work with fake timers | |
vi.spyOn(global, 'requestAnimationFrame').mockImplementation((cb) => { | |
setTimeout(cb, 16); | |
return 1; // return a number as expected by the interface | |
}); | |
interface PropertyFormValues { | |
propertyTitle: string; | |
}; | |
// Child component that uses the hook inside the FormProvider context | |
const FormChildComponent = ({ | |
onReady, | |
}: { | |
onReady: (resetAsync: ReturnType<typeof useResetFormAsync>) => void; | |
}) => { | |
const resetAsync = useResetFormAsync(); | |
useEffect(() => { | |
onReady(resetAsync); | |
}, [resetAsync, onReady]); | |
return <form />; | |
}; | |
const PropertyFormWrapper = ({ | |
onReady, | |
}: { | |
onReady: ( | |
resetAsync: ReturnType<typeof useResetFormAsync>, | |
methods: ReturnType<typeof useForm<PropertyFormValues>> | |
) => void; | |
}) => { | |
const methods = useForm<PropertyFormValues>({ | |
defaultValues: { propertyTitle: 'Sunny Villa' }, | |
}); | |
const handleChildReady = (resetAsync: ReturnType<typeof useResetFormAsync>) => { | |
onReady(resetAsync, methods); | |
}; | |
return ( | |
<FormProvider {...methods}> | |
<FormChildComponent onReady={handleChildReady} /> | |
</FormProvider> | |
); | |
}; | |
describe('useResetFormAsync', () => { | |
let resetPropertyForm: ReturnType<typeof useResetFormAsync>; | |
let propertyFormMethods: ReturnType<typeof useForm<PropertyFormValues>>; | |
beforeEach(() => { | |
render( | |
<PropertyFormWrapper | |
onReady={(resetFn, methods) => { | |
resetPropertyForm = resetFn; | |
propertyFormMethods = methods; | |
}} | |
/> | |
); | |
}); | |
afterEach(() => { | |
vi.clearAllTimers(); | |
vi.clearAllMocks(); | |
}); | |
it('resolves once property form is clean again', async () => { | |
// Make the form dirty first | |
act(() => { | |
propertyFormMethods.setValue('propertyTitle', 'Cozy Cottage'); | |
}); | |
let isResolved = false; | |
const resetPromise = resetPropertyForm({ propertyTitle: 'Sunny Villa' }).then(() => { | |
isResolved = true; | |
}); | |
// Wait for the reset and requestAnimationFrame to process | |
await act(async () => { | |
// Advance timers to trigger the requestAnimationFrame | |
for (let i = 0; i < 5; i++) { | |
vi.advanceTimersToNextTimer(); | |
} | |
}); | |
await resetPromise; | |
expect(isResolved).toBeTruthy(); | |
}); | |
it('throws if keepDirty is used on property form reset', () => { | |
expect(() => | |
resetPropertyForm( | |
{ propertyTitle: 'Sunny Villa' }, | |
{ keepDirty: true } | |
) | |
).toThrow(/Do not use it with the `keepDirty` option/); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useRef, useEffect } from 'react'; | |
import { useFormContext, FieldValues, KeepStateOptions } from 'react-hook-form'; | |
/** | |
* A hook to reset the form asynchronously. | |
* | |
* Context: | |
* Form reset takes an unpredictable time to complete as it depends on the form size and complexity. | |
* This hook is a workaround to ensure that the next step will be executed only after the form has been reset. | |
* | |
* When to use: | |
* - You want to reset the form and do the next step only AFTER the form is reset | |
* | |
* When NOT to use it: | |
* - You just want to reset the form. Use `form.reset()` instead. | |
* - The form is most likely not dirty, but you want to swap the form values. | |
* - You want to reset the form but keep the dirty state. | |
* Use `form.reset(newInitialValues, { keepDirty: true })` instead. | |
* | |
* @example | |
* const resetAsync = useResetFormAsync(); | |
* | |
* resetAsync().then(() => { | |
* console.log('Form reset'); | |
* }); | |
* | |
* OR | |
* | |
* resetAsync(newInitialValues, resetOptions); // These params are directly passed to the form.reset method | |
* | |
* WARNING: | |
* This util checks against the isDirty state. | |
* DO NOT use it with the `keepDirty` option | |
* | |
* resetAsync(newInitialValues, { keepDirty: true }); | |
* | |
* This will lead to an infinite loop and will throw an error | |
* | |
*/ | |
export const useResetFormAsync = () => { | |
const form = useFormContext(); | |
const { isDirty } = form.formState; | |
const isDirtyRef = useRef(isDirty); | |
useEffect(() => { | |
isDirtyRef.current = isDirty; | |
}, [isDirty]); | |
const resetAsync = ( | |
values?: FieldValues, | |
options?: KeepStateOptions | |
): Promise<void> => { | |
if (options?.keepDirty) { | |
throw new Error('useResetFormAsync - Do not use it with the `keepDirty` option. Reed more in the hook comments.'); | |
} | |
return new Promise((resolve, reject) => { | |
let tick = 0; | |
form.reset(values, options); | |
const check = () => { | |
tick++; | |
if (isDirtyRef.current) { | |
if (tick > 1000) { | |
reject(new Error('Form is still dirty after 1000 ticks')); | |
} else { | |
requestAnimationFrame(check); | |
} | |
} else { | |
resolve(); | |
} | |
}; | |
check(); | |
}); | |
}; | |
return resetAsync; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment