From 181e953af683cf04f7e0d014025ba162cb4ba044 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 11 Jan 2023 10:36:03 -0500 Subject: [PATCH] Fetchers should persist data through reload/resubmit --- .changeset/eighty-donkeys-run.md | 5 ++ integration/fetcher-test.ts | 120 +++++++++++++++++++++++++++- packages/remix-react/components.tsx | 6 +- packages/remix-react/transition.ts | 2 +- 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 .changeset/eighty-donkeys-run.md diff --git a/.changeset/eighty-donkeys-run.md b/.changeset/eighty-donkeys-run.md new file mode 100644 index 00000000000..679c3e71745 --- /dev/null +++ b/.changeset/eighty-donkeys-run.md @@ -0,0 +1,5 @@ +--- +"@remix-run/react": patch +--- + +Fetchers should persist data through reload/resubmit diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts index a8e9a9520f6..ed43829831a 100644 --- a/integration/fetcher-test.ts +++ b/integration/fetcher-test.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -143,6 +143,57 @@ test.describe("useFetcher", () => { ); } `, + + "app/routes/fetcher-echo.jsx": js` + import { json } from "@remix-run/node"; + import { useFetcher } from "@remix-run/react"; + + export async function action({ request }) { + await new Promise(r => setTimeout(r, 1000)); + let value = (await request.formData()).get('value'); + return json({ data: "ACTION " + value }) + } + + export async function loader({ request }) { + await new Promise(r => setTimeout(r, 1000)); + let value = new URL(request.url).searchParams.get('value'); + return json({ data: "LOADER " + value }) + } + + export default function Index() { + let fetcherValues = []; + if (typeof window !== 'undefined') { + if (!window.fetcherValues) { + window.fetcherValues = []; + } + fetcherValues = window.fetcherValues + } + + let fetcher = useFetcher(); + + let currentValue = fetcher.state + '/' + fetcher.data?.data; + if (fetcherValues[fetcherValues.length - 1] !== currentValue) { + fetcherValues.push(currentValue) + } + + return ( + <> + + + + + {fetcher.state === 'idle' ?

IDLE

: null} +
{JSON.stringify(fetcherValues)}
+ + ); + } + `, }, }); @@ -232,4 +283,71 @@ test.describe("useFetcher", () => { await app.clickElement("#submit-index-post"); await page.waitForSelector(`pre:has-text("${PARENT_INDEX_ACTION}")`); }); + + test("fetcher.load persists data through reloads", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + + await app.goto("/fetcher-echo", true); + expect(await app.getHtml("pre")).toMatch( + JSON.stringify(["idle/undefined"]) + ); + + await page.fill("#fetcher-input", "1"); + await app.clickElement("#fetcher-load"); + await page.waitForSelector("#fetcher-idle"); + expect(await app.getHtml("pre")).toMatch( + JSON.stringify(["idle/undefined", "loading/undefined", "idle/LOADER 1"]) + ); + + await page.fill("#fetcher-input", "2"); + await app.clickElement("#fetcher-load"); + await page.waitForSelector("#fetcher-idle"); + expect(await app.getHtml("pre")).toMatch( + JSON.stringify([ + "idle/undefined", + "loading/undefined", + "idle/LOADER 1", + "loading/LOADER 1", // Preserves old data during reload + "idle/LOADER 2", + ]) + ); + }); + + test("fetcher.submit persists data through resubmissions", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + + await app.goto("/fetcher-echo", true); + expect(await app.getHtml("pre")).toMatch( + JSON.stringify(["idle/undefined"]) + ); + + await page.fill("#fetcher-input", "1"); + await app.clickElement("#fetcher-submit"); + await page.waitForSelector("#fetcher-idle"); + expect(await app.getHtml("pre")).toMatch( + JSON.stringify([ + "idle/undefined", + "submitting/undefined", + "loading/ACTION 1", + "idle/ACTION 1", + ]) + ); + + await page.fill("#fetcher-input", "2"); + await app.clickElement("#fetcher-submit"); + await page.waitForSelector("#fetcher-idle"); + expect(await app.getHtml("pre")).toMatch( + JSON.stringify([ + "idle/undefined", + "submitting/undefined", + "loading/ACTION 1", + "idle/ACTION 1", + "submitting/ACTION 1", // Preserves old data during resubmissions + "loading/ACTION 2", + "idle/ACTION 2", + ]) + ); + }); }); diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index f9c051fe6bb..ad65ab32c4f 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -1202,7 +1202,7 @@ function convertRouterFetcherToRemixFetcher( formData: formData, key: "", }, - data: undefined, + data, }; return fetcher; } else { @@ -1285,7 +1285,7 @@ function convertRouterFetcherToRemixFetcher( formData: formData, key: "", }, - data: undefined, + data, }; return fetcher; } @@ -1297,7 +1297,7 @@ function convertRouterFetcherToRemixFetcher( state: "loading", type: "normalLoad", submission: undefined, - data: undefined, + data, }; return fetcher; } diff --git a/packages/remix-react/transition.ts b/packages/remix-react/transition.ts index fb052fc8523..8b0ec322db3 100644 --- a/packages/remix-react/transition.ts +++ b/packages/remix-react/transition.ts @@ -120,7 +120,7 @@ export type FetcherStates = { formData: FormData; formEncType: string; submission: ActionSubmission; - data: undefined; + data: TData | undefined; }; SubmittingLoader: { state: "submitting";