diff --git a/.changeset/blue-guests-raise.md b/.changeset/blue-guests-raise.md
new file mode 100644
index 00000000000..ad2b1441581
--- /dev/null
+++ b/.changeset/blue-guests-raise.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/dev": patch
+---
+
+Support HMR for routes with `handle` export in Vite dev
diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts
index 0edf0593afa..6ab6bbf4fc4 100644
--- a/integration/vite-dev-test.ts
+++ b/integration/vite-dev-test.ts
@@ -197,6 +197,36 @@ test.describe("Vite dev", () => {
>
}
`,
+ "app/routes/known-route-exports.tsx": js`
+ import { useMatches } from "@remix-run/react";
+
+ export const meta = () => [{
+ title: "HMR meta: 0"
+ }]
+
+ export const links = () => [{
+ rel: "icon",
+ href: "/favicon.ico",
+ type: "image/png",
+ "data-link": "HMR links: 0",
+ }]
+
+ export const handle = {
+ data: "HMR handle: 0"
+ };
+
+ export default function TestRoute() {
+ const matches = useMatches();
+
+ return (
+
+
+
HMR component: 0
+
{matches[1].handle.data}
+
+ );
+ }
+ `,
},
});
@@ -354,6 +384,55 @@ test.describe("Vite dev", () => {
expect(pageErrors).toEqual([]);
});
+
+ test("handle known route exports with HMR", async ({ page }) => {
+ let pageErrors: unknown[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
+
+ await page.goto(`http://localhost:${devPort}/known-route-exports`, {
+ waitUntil: "networkidle",
+ });
+ expect(pageErrors).toEqual([]);
+
+ // file editing utils
+ let filepath = path.join(projectDir, "app/routes/known-route-exports.tsx");
+ let filedata = await fs.readFile(filepath, "utf8");
+ async function editFile(edit: (data: string) => string) {
+ filedata = edit(filedata);
+ await fs.writeFile(filepath, filedata, "utf8");
+ }
+
+ // verify input state is preserved after each update
+ let input = page.locator("input");
+ await input.type("stateful");
+ await expect(input).toHaveValue("stateful");
+
+ // component
+ await editFile((data) =>
+ data.replace("HMR component: 0", "HMR component: 1")
+ );
+ await expect(page.locator("[data-hmr]")).toHaveText("HMR component: 1");
+ await expect(input).toHaveValue("stateful");
+
+ // handle
+ await editFile((data) => data.replace("HMR handle: 0", "HMR handle: 1"));
+ await expect(page.locator("[data-handle]")).toHaveText("HMR handle: 1");
+ await expect(input).toHaveValue("stateful");
+
+ // meta
+ await editFile((data) => data.replace("HMR meta: 0", "HMR meta: 1"));
+ await expect(page).toHaveTitle("HMR meta: 1");
+ await expect(input).toHaveValue("stateful");
+
+ // links
+ await editFile((data) => data.replace("HMR links: 0", "HMR links: 1"));
+ await expect(page.locator("[data-link]")).toHaveAttribute(
+ "data-link",
+ "HMR links: 1"
+ );
+
+ expect(pageErrors).toEqual([]);
+ });
});
let bufferize = (stream: Readable): (() => string) => {
diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts
index ae98042b9c5..61eb4a65f22 100644
--- a/packages/remix-dev/vite/plugin.ts
+++ b/packages/remix-dev/vite/plugin.ts
@@ -1092,7 +1092,9 @@ function addRefreshWrapper(
id: string
): string {
let isRoute = getRoute(pluginConfig, id);
- let acceptExports = isRoute ? ["meta", "links", "shouldRevalidate"] : [];
+ let acceptExports = isRoute
+ ? ["handle", "meta", "links", "shouldRevalidate"]
+ : [];
return (
REACT_REFRESH_HEADER.replace("__SOURCE__", JSON.stringify(id)) +
code +