Skip to content

Commit

Permalink
Add useDebounceSubmit (sergiodxa#291)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jacobparis authored Dec 19, 2023
1 parent 8028e92 commit b3a1253
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 5 deletions.
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 (
<input
name={name}
type="text"
onChange={(event) => {
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**
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
55 changes: 55 additions & 0 deletions src/react/use-debounce-submit.ts
Original file line number Diff line number Diff line change
@@ -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<SubmitFunction>["0"];

export function useDebounceSubmit() {
let timeoutRef = useRef<NodeJS.Timeout | undefined>();

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 `<form>` to be submitted to the server, a specific
* `<button>` or `<input type="submit">` to use to submit the form, or some
* arbitrary data to submit.
*
* Note: When using a `<button>` its `name` and `value` will also be
* included in the form data that is submitted.
*/
target: SubmitTarget,
/**
* Options that override the `<form>`'s own attributes. Required when
* submitting arbitrary data without a backing `<form>`. Additionally, you
* can specify a `debounceTimeout` to delay the submission of the data.
*/
options?: SubmitOptions & {
/** Submissions within this timeout will be canceled */
debounceTimeout?: number;
},
) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
if (!options?.debounceTimeout || options.debounceTimeout <= 0) {
return originalSubmit(target, options);
}

timeoutRef.current = setTimeout(() => {
originalSubmit(target, options);
}, options.debounceTimeout);
},
[originalSubmit],
);

return submit;
}

0 comments on commit b3a1253

Please sign in to comment.