Skip to content

Commit 5dc3c2e

Browse files
committed
Add fetcher.unstable_reset API
1 parent c208585 commit 5dc3c2e

File tree

6 files changed

+215
-3
lines changed

6 files changed

+215
-3
lines changed

.changeset/fetcher-reset.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
[UNSTABLE} Add `fetcher.unstable_reset()` API

packages/react-router/__tests__/dom/data-browser-router-test.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5487,6 +5487,76 @@ function testDomRouter(
54875487
expect(html).toContain("fetcher count:1");
54885488
});
54895489

5490+
it("resets a fetcher", async () => {
5491+
let router = createTestRouter(
5492+
[
5493+
{
5494+
path: "/",
5495+
Component() {
5496+
let fetcher = useFetcher();
5497+
return (
5498+
<>
5499+
<p id="output">{`${fetcher.state}-${fetcher.data}`}</p>
5500+
<button onClick={() => fetcher.load("/")}>load</button>
5501+
<button onClick={() => fetcher.unstable_reset()}>
5502+
reset
5503+
</button>
5504+
</>
5505+
);
5506+
},
5507+
async loader() {
5508+
return "FETCH";
5509+
},
5510+
},
5511+
],
5512+
{
5513+
window: getWindow("/"),
5514+
hydrationData: { loaderData: { "0": null } },
5515+
},
5516+
);
5517+
let { container } = render(<RouterProvider router={router} />);
5518+
5519+
expect(getHtml(container.querySelector("#output")!))
5520+
.toMatchInlineSnapshot(`
5521+
"<p
5522+
id="output"
5523+
>
5524+
idle-undefined
5525+
</p>"
5526+
`);
5527+
5528+
fireEvent.click(screen.getByText("load"));
5529+
expect(getHtml(container.querySelector("#output")!))
5530+
.toMatchInlineSnapshot(`
5531+
"<p
5532+
id="output"
5533+
>
5534+
loading-undefined
5535+
</p>"
5536+
`);
5537+
5538+
await waitFor(() => screen.getByText(/idle/));
5539+
expect(getHtml(container.querySelector("#output")!))
5540+
.toMatchInlineSnapshot(`
5541+
"<p
5542+
id="output"
5543+
>
5544+
idle-FETCH
5545+
</p>"
5546+
`);
5547+
5548+
fireEvent.click(screen.getByText("reset"));
5549+
await waitFor(() => screen.getByText(/idle/));
5550+
expect(getHtml(container.querySelector("#output")!))
5551+
.toMatchInlineSnapshot(`
5552+
"<p
5553+
id="output"
5554+
>
5555+
idle-null
5556+
</p>"
5557+
`);
5558+
});
5559+
54905560
describe("useFetcher({ key })", () => {
54915561
it("generates unique keys for fetchers by default", async () => {
54925562
let dfd1 = createDeferred();

packages/react-router/__tests__/router/fetchers-test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3555,4 +3555,103 @@ describe("fetchers", () => {
35553555
expect((await request.formData()).get("a")).toBe("1");
35563556
});
35573557
});
3558+
3559+
describe("resetFetcher", () => {
3560+
it("resets fetcher data", async () => {
3561+
let t = setup({
3562+
routes: [
3563+
{ id: "root", path: "/" },
3564+
{ id: "fetch", path: "/fetch", loader: true },
3565+
],
3566+
});
3567+
3568+
let A = await t.fetch("/fetch", "a", "root");
3569+
expect(t.fetchers["a"]).toMatchObject({
3570+
state: "loading",
3571+
data: undefined,
3572+
});
3573+
3574+
await A.loaders.fetch.resolve("FETCH");
3575+
expect(t.fetchers["a"]).toMatchObject({
3576+
state: "idle",
3577+
data: "FETCH",
3578+
});
3579+
3580+
t.router.resetFetcher("a");
3581+
expect(t.fetchers["a"]).toMatchObject({
3582+
state: "idle",
3583+
data: null,
3584+
});
3585+
});
3586+
3587+
it("aborts in-flight fetchers (first call)", async () => {
3588+
let t = setup({
3589+
routes: [
3590+
{ id: "root", path: "/" },
3591+
{ id: "fetch", path: "/fetch", loader: true },
3592+
],
3593+
});
3594+
3595+
let A = await t.fetch("/fetch", "a", "root");
3596+
expect(t.fetchers["a"]).toMatchObject({
3597+
state: "loading",
3598+
data: undefined,
3599+
});
3600+
3601+
t.router.resetFetcher("a");
3602+
expect(t.fetchers["a"]).toMatchObject({
3603+
state: "idle",
3604+
data: null,
3605+
});
3606+
3607+
// no-op
3608+
await A.loaders.fetch.resolve("FETCH");
3609+
expect(t.fetchers["a"]).toMatchObject({
3610+
state: "idle",
3611+
data: null,
3612+
});
3613+
expect(A.loaders.fetch.signal.aborted).toBe(true);
3614+
});
3615+
3616+
it("aborts in-flight fetchers (subsequent call)", async () => {
3617+
let t = setup({
3618+
routes: [
3619+
{ id: "root", path: "/" },
3620+
{ id: "fetch", path: "/fetch", loader: true },
3621+
],
3622+
});
3623+
3624+
let A = await t.fetch("/fetch", "a", "root");
3625+
expect(t.fetchers["a"]).toMatchObject({
3626+
state: "loading",
3627+
data: undefined,
3628+
});
3629+
3630+
await A.loaders.fetch.resolve("FETCH");
3631+
expect(t.fetchers["a"]).toMatchObject({
3632+
state: "idle",
3633+
data: "FETCH",
3634+
});
3635+
3636+
let B = await t.fetch("/fetch", "a", "root");
3637+
expect(t.fetchers["a"]).toMatchObject({
3638+
state: "loading",
3639+
data: "FETCH",
3640+
});
3641+
3642+
t.router.resetFetcher("a");
3643+
expect(t.fetchers["a"]).toMatchObject({
3644+
state: "idle",
3645+
data: null,
3646+
});
3647+
3648+
// no-op
3649+
await B.loaders.fetch.resolve("FETCH*");
3650+
expect(t.fetchers["a"]).toMatchObject({
3651+
state: "idle",
3652+
data: null,
3653+
});
3654+
expect(B.loaders.fetch.signal.aborted).toBe(true);
3655+
});
3656+
});
35583657
});

packages/react-router/lib/dom/lib.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2711,6 +2711,18 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
27112711
flushSync?: boolean;
27122712
},
27132713
) => Promise<void>;
2714+
2715+
/**
2716+
* Reset a fetcher back to an empty/idle state.
2717+
*
2718+
* If the fetcher is currently in-flight, the
2719+
* [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
2720+
* will be aborted with the `reason`, if provided.
2721+
*
2722+
* @param reason Optional `reason` to provide to [`AbortController.abort()`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort)
2723+
* @returns void
2724+
*/
2725+
unstable_reset: (reason?: unknown) => void;
27142726
};
27152727

27162728
// TODO: (v7) Change the useFetcher generic default from `any` to `unknown`
@@ -2745,6 +2757,9 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
27452757
* method: "post",
27462758
* encType: "application/json"
27472759
* })
2760+
*
2761+
* // reset fetcher
2762+
* fetcher.unstable_reset()
27482763
* }
27492764
*
27502765
* @public
@@ -2826,6 +2841,10 @@ export function useFetcher<T = any>({
28262841
[fetcherKey, submitImpl],
28272842
);
28282843

2844+
let unstable_reset = React.useCallback<
2845+
FetcherWithComponents<T>["unstable_reset"]
2846+
>((reason) => router.resetFetcher(fetcherKey, reason), [router, fetcherKey]);
2847+
28292848
let FetcherForm = React.useMemo(() => {
28302849
let FetcherForm = React.forwardRef<HTMLFormElement, FetcherFormProps>(
28312850
(props, ref) => {
@@ -2846,10 +2865,11 @@ export function useFetcher<T = any>({
28462865
Form: FetcherForm,
28472866
submit,
28482867
load,
2868+
unstable_reset,
28492869
...fetcher,
28502870
data,
28512871
}),
2852-
[FetcherForm, submit, load, fetcher, data],
2872+
[FetcherForm, submit, load, unstable_reset, fetcher, data],
28532873
);
28542874

28552875
return fetcherWithComponents;

packages/react-router/lib/dom/server.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ export function createStaticRouter(
472472
deleteFetcher() {
473473
throw msg("deleteFetcher");
474474
},
475+
resetFetcher() {
476+
throw msg("resetFetcher");
477+
},
475478
dispose() {
476479
throw msg("dispose");
477480
},

packages/react-router/lib/router/router.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,15 @@ export interface Router {
215215
*/
216216
getFetcher<TData = any>(key: string): Fetcher<TData>;
217217

218+
/**
219+
* @internal
220+
* PRIVATE - DO NOT USE
221+
*
222+
* Reset the fetcher for a given key
223+
* @param key
224+
*/
225+
resetFetcher(key: string, reason?: unknown): void;
226+
218227
/**
219228
* @private
220229
* PRIVATE - DO NOT USE
@@ -3059,6 +3068,11 @@ export function createRouter(init: RouterInit): Router {
30593068
return state.fetchers.get(key) || IDLE_FETCHER;
30603069
}
30613070

3071+
function resetFetcher(key: string, reason?: unknown) {
3072+
abortFetcher(key, reason);
3073+
updateFetcherState(key, getDoneFetcher(null));
3074+
}
3075+
30623076
function deleteFetcher(key: string): void {
30633077
let fetcher = state.fetchers.get(key);
30643078
// Don't abort the controller if this is a deletion of a fetcher.submit()
@@ -3089,10 +3103,10 @@ export function createRouter(init: RouterInit): Router {
30893103
updateState({ fetchers: new Map(state.fetchers) });
30903104
}
30913105

3092-
function abortFetcher(key: string) {
3106+
function abortFetcher(key: string, reason?: unknown) {
30933107
let controller = fetchControllers.get(key);
30943108
if (controller) {
3095-
controller.abort();
3109+
controller.abort(reason);
30963110
fetchControllers.delete(key);
30973111
}
30983112
}
@@ -3472,6 +3486,7 @@ export function createRouter(init: RouterInit): Router {
34723486
createHref: (to: To) => init.history.createHref(to),
34733487
encodeLocation: (to: To) => init.history.encodeLocation(to),
34743488
getFetcher,
3489+
resetFetcher,
34753490
deleteFetcher: queueFetcherForDeletion,
34763491
dispose,
34773492
getBlocker,

0 commit comments

Comments
 (0)