From b3a12539fc6fa1c1baed1ab72b40c4cd16f0a815 Mon Sep 17 00:00:00 2001 From: Jacob Paris Date: Tue, 19 Dec 2023 13:21:51 +0800 Subject: [PATCH] Add useDebounceSubmit (#291) Since we can now set `navigate: false` on useSubmit to get it to use fetchers under the hood (and not navigate the user) use-cases for debounced submission have opened up This is identical to the useDebounceFetcher hook except it modifies useSubmit instead --- README.md | 39 +++++++++++++++++++--- package.json | 1 + src/react/use-debounce-submit.ts | 55 ++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/react/use-debounce-submit.ts diff --git a/README.md b/README.md index a28e8a7..76a9d2b 100644 --- a/README.md +++ b/README.md @@ -1759,16 +1759,14 @@ export default function Component() { Now you can see in your DevTools that when the user hovers an anchor it will prefetch it, and when the user clicks it will do a client-side navigation. -### Debounced Fetcher +### Debounced Fetcher and Submit > **Note** > This depends on `react`, and `@remix-run/react`. -The `useDebounceFetcher` is a wrapper of `useFetcher` that adds debounce support to `fetcher.submit`. +`useDebounceFetcher` and `useDebounceSubmit` are wrappers of `useFetcher` and `useSubmit` that add debounce support. -The hook is based on [@JacobParis](https://github.com/JacobParis)' [article](https://www.jacobparis.com/content/use-debounce-fetcher). - -The main difference with Jacob's version is that Remix Utils' version overwrites `fetcher.submit` instead of appending a `fetcher.debounceSubmit` method. +These hooks are based on [@JacobParis](https://github.com/JacobParis)' [article](https://www.jacobparis.com/content/use-debounce-fetcher). ```tsx import { useDebounceFetcher } from "remix-utils/use-debounce-fetcher"; @@ -1788,6 +1786,37 @@ export function Component({ data }) { } ``` +Usage with `useDebounceSubmit` is similar. + +```tsx +import { useDebounceSubmit } from "remix-utils/use-debounce-submit"; + +export function Component({ name }) { + let submit = useDebounceSubmit(); + + return ( + { + submit(event.target.form, { + navigate: false, // use a fetcher instead of a page navigation + fetcherKey: name, // cancel any previous fetcher with the same key + debounceTimeout: 1000, + }); + }} + onBlur={() => { + submit(event.target.form, { + navigate: false, + fetcherKey: name, + debounceTimeout: 0, // submit immediately, canceling any pending fetcher + }); + }} + /> + ); +} +``` + ### Derive Fetcher Type > **Note** diff --git a/package.json b/package.json index 23c80c3..87fbd17 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "./fetcher-type": "./build/react/fetcher-type.js", "./server-only": "./build/react/server-only.js", "./use-debounce-fetcher": "./build/react/use-debounce-fetcher.js", + "./use-debounce-submit": "./build/react/use-debounce-submit.js", "./use-delegated-anchors": "./build/react/use-delegated-anchors.js", "./use-global-navigation-state": "./build/react/use-global-navigation-state.js", "./use-hydrated": "./build/react/use-hydrated.js", diff --git a/src/react/use-debounce-submit.ts b/src/react/use-debounce-submit.ts new file mode 100644 index 0000000..6d4e020 --- /dev/null +++ b/src/react/use-debounce-submit.ts @@ -0,0 +1,55 @@ +import type { SubmitOptions, SubmitFunction } from "@remix-run/react"; +import { useSubmit } from "@remix-run/react"; +import { useCallback, useEffect, useRef } from "react"; + +type SubmitTarget = Parameters["0"]; + +export function useDebounceSubmit() { + let timeoutRef = useRef(); + + useEffect(() => { + // no initialize step required since timeoutRef defaults undefined + let timeout = timeoutRef.current; + return () => { + if (timeout) clearTimeout(timeout); + }; + }, [timeoutRef]); + + // Clone the original submit to avoid a recursive loop + const originalSubmit = useSubmit(); + + const submit = useCallback( + ( + /** + * Specifies the `
` to be submitted to the server, a specific + * `