Repository: remix-run/remix (ts)

Issue:
JsonFunction not exported from remix-node as type
### What version of Remix are you using?

1.7.2

### Steps to Reproduce

`npm install @remix-run/node`

### Expected Behavior

JsonFunction should be exported as a type from `@remix-run/node`

### Actual Behavior

As you can see here [remix-node/index.ts#L44](https://github.com/remix-run/remix/blob/main/packages/remix-node/index.ts#L44), JsonFunction is not exported as a type.

Pull request:
fix(remix-node): export `JsonFunction` as type instead of as value
Closes #4375


Patch:
diff --git a/.changeset/breezy-dancers-lie.md b/.changeset/breezy-dancers-lie.md
new file mode 100644
index 000000000..a836b0932
--- /dev/null
+++ b/.changeset/breezy-dancers-lie.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/cloudflare": minor
+---
+
+Rename `createCloudflareKVSessionStorage` to `createWorkersKVSessionStorage`
diff --git a/.changeset/old-rice-drive.md b/.changeset/old-rice-drive.md
new file mode 100644
index 000000000..a23d4b9a6
--- /dev/null
+++ b/.changeset/old-rice-drive.md
@@ -0,0 +1,6 @@
+---
+"remix": patch
+"@remix-run/node": patch
+---
+
+Export `JsonFunction` as a type in `@remix-run/node`
diff --git a/.changeset/short-bobcats-switch.md b/.changeset/short-bobcats-switch.md
new file mode 100644
index 000000000..b5d82a9cf
--- /dev/null
+++ b/.changeset/short-bobcats-switch.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/server-runtime": minor
+---
+
+remove `react` & `react-dom` from `peerDependencies`
diff --git a/.changeset/tender-turtles-wash.md b/.changeset/tender-turtles-wash.md
new file mode 100644
index 000000000..9e7a10e0a
--- /dev/null
+++ b/.changeset/tender-turtles-wash.md
@@ -0,0 +1,11 @@
+---
+"remix": patch
+"@remix-run/dev": patch
+---
+
+When running the dev server (current or `unstable_dev`), each rebuild wrote new files to `build/` and `public/build/`.
+Since these files are not removed (unless the dev server crashes or is gracefully terminated),
+thousands of files could accumulate as the dev server ran.
+This causes performance issues and could be confusing.
+
+Now, the dev server also cleans up the build directories whenever a rebuild starts.
diff --git a/.changeset/wild-foxes-wait.md b/.changeset/wild-foxes-wait.md
new file mode 100644
index 000000000..df75c7379
--- /dev/null
+++ b/.changeset/wild-foxes-wait.md
@@ -0,0 +1,6 @@
+---
+"remix": patch
+"@remix-run/react": patch
+---
+
+Configure the browser-side live reload socket timeout via the new `timeoutMs` prop on `<LiveReload />`
diff --git a/docs/api/remix.md b/docs/api/remix.md
index 880d960f5..c4eb41b4b 100644
--- a/docs/api/remix.md
+++ b/docs/api/remix.md
@@ -258,7 +258,7 @@ title: Remix Packages
 
 [Moved →][moved-59]
 
-### `createCloudflareKVSessionStorage` (cloudflare-workers)
+### `createWorkersKVSessionStorage` (Cloudflare Workers)
 
 [Moved →][moved-60]
 
@@ -354,7 +354,7 @@ title: Remix Packages
 [moved-57]: ../utils/sessions#createcookiesessionstorage
 [moved-58]: ../utils/sessions#creatememorysessionstorage
 [moved-59]: ../utils/sessions#createfilesessionstorage-node
-[moved-60]: ../utils/sessions#createcloudflarekvsessionstorage-cloudflare-workers
+[moved-60]: ../utils/sessions#createworkerskvsessionstorage-cloudflare-workers
 [moved-61]: ../utils/sessions#createarctablesessionstorage-architect-amazon-dynamodb
 [moved-62]: ../utils/sessions#session-api
 [moved-63]: ../utils/sessions#sessionhaskey
diff --git a/docs/utils/sessions.md b/docs/utils/sessions.md
index ac5beef9c..af56f4f9e 100644
--- a/docs/utils/sessions.md
+++ b/docs/utils/sessions.md
@@ -13,7 +13,7 @@ Remix comes with several pre-built session storage options for common scenarios,
 - `createCookieSessionStorage`
 - `createMemorySessionStorage`
 - `createFileSessionStorage` (node)
-- `createCloudflareKVSessionStorage` (cloudflare-workers)
+- `createWorkersKVSessionStorage` (Cloudflare Workers)
 - `createArcTableSessionStorage` (architect, Amazon DynamoDB)
 - custom storage with `createSessionStorage`
 
@@ -330,16 +330,16 @@ const { getSession, commitSession, destroySession } =
 export { getSession, commitSession, destroySession };
 ```
 
-## `createCloudflareKVSessionStorage` (cloudflare-workers)
+## `createWorkersKVSessionStorage` (Cloudflare Workers)
 
-For [Cloudflare KV][cloudflare-kv] backed sessions, use `createCloudflareKVSessionStorage()`.
+For [Cloudflare Workers KV][cloudflare-kv] backed sessions, use `createWorkersKVSessionStorage()`.
 
 The advantage of KV backed sessions is that only the session ID is stored in the cookie while the rest of the data is stored in a globally replicated, low-latency data store with exceptionally high read volumes with low-latency.
 
 ```js filename=app/sessions.server.js
 import {
   createCookie,
-  createCloudflareKVSessionStorage,
+  createWorkersKVSessionStorage,
 } from "@remix-run/cloudflare";
 
 // In this example the Cookie is created separately.
@@ -349,7 +349,7 @@ const sessionCookie = createCookie("__session", {
 });
 
 const { getSession, commitSession, destroySession } =
-  createCloudflareKVSessionStorage({
+  createWorkersKVSessionStorage({
     // The KV Namespace where you want to store sessions
     kv: YOUR_NAMESPACE,
     cookie: sessionCookie,
diff --git a/packages/remix-cloudflare/index.ts b/packages/remix-cloudflare/index.ts
index 9f00786cc..082730667 100644
--- a/packages/remix-cloudflare/index.ts
+++ b/packages/remix-cloudflare/index.ts
@@ -1,6 +1,21 @@
 import "./globals";
 
-export { createCloudflareKVSessionStorage } from "./sessions/cloudflareKVSessionStorage";
+import { createWorkersKVSessionStorage } from "./sessions/workersKVStorage";
+
+const warn = <T extends Function>(fn: T, message: string): T =>
+  ((...args: unknown[]) => {
+    console.warn(message);
+
+    return fn(...args);
+  }) as unknown as T;
+
+/** @deprecated Use `createWorkersKVSessionStorage` instead. */
+export const createCloudflareKVSessionStorage = warn(
+  createWorkersKVSessionStorage,
+  "`createCloudflareKVSessionStorage` is deprecated. Please use `createWorkersKVSessionStorage` instead."
+);
+
+export { createWorkersKVSessionStorage } from "./sessions/workersKVStorage";
 
 export {
   createCookie,
diff --git a/packages/remix-cloudflare/sessions/cloudflareKVSessionStorage.ts b/packages/remix-cloudflare/sessions/workersKVStorage.ts
similarity index 92%
rename from packages/remix-cloudflare/sessions/cloudflareKVSessionStorage.ts
rename to packages/remix-cloudflare/sessions/workersKVStorage.ts
index 455c74e39..45a4573b4 100644
--- a/packages/remix-cloudflare/sessions/cloudflareKVSessionStorage.ts
+++ b/packages/remix-cloudflare/sessions/workersKVStorage.ts
@@ -5,7 +5,7 @@ import type {
 
 import { createSessionStorage } from "../implementations";
 
-interface CloudflareKVSessionStorageOptions {
+interface WorkersKVSessionStorageOptions {
   /**
    * The Cookie used to store the session id on the client, or options used
    * to automatically create one.
@@ -24,10 +24,10 @@ interface CloudflareKVSessionStorageOptions {
  * The advantage of using this instead of cookie session storage is that
  * KV Store may contain much more data than cookies.
  */
-export function createCloudflareKVSessionStorage({
+export function createWorkersKVSessionStorage({
   cookie,
   kv,
-}: CloudflareKVSessionStorageOptions): SessionStorage {
+}: WorkersKVSessionStorageOptions): SessionStorage {
   return createSessionStorage({
     cookie,
     async createData(data, expires) {
diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts
index 2e099683e..4c9280202 100644
--- a/packages/remix-dev/cli/commands.ts
+++ b/packages/remix-dev/cli/commands.ts
@@ -188,11 +188,12 @@ export async function watch(
       ? remixRootOrConfig
       : await readConfig(remixRootOrConfig);
 
-  return devServer.liveReload(config, {
+  devServer.liveReload(config, {
     mode,
     onInitialBuild: (durationMs) =>
       console.log(`💿 Built in ${prettyMs(durationMs)}`),
   });
+  return await new Promise(() => {});
 }
 
 export async function dev(
diff --git a/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts b/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts
index 810c810bb..5cae22956 100644
--- a/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts
+++ b/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts
@@ -100,7 +100,10 @@ const defaultRuntimeExports: ExportNames = {
 
 const exportNamesByRuntime: Record<Runtime, Partial<ExportNames>> = {
   cloudflare: {
-    value: ["createCloudflareKVSessionStorage"],
+    value: [
+      "createCloudflareKVSessionStorage",
+      "createWorkersKVSessionStorage",
+    ],
   },
   node: {
     type: ["HeadersInit", "RequestInfo", "RequestInit", "ResponseInit"],
diff --git a/packages/remix-dev/compiler/utils/fs.ts b/packages/remix-dev/compiler/utils/fs.ts
index bf38d480a..c64ac0622 100644
--- a/packages/remix-dev/compiler/utils/fs.ts
+++ b/packages/remix-dev/compiler/utils/fs.ts
@@ -9,17 +9,3 @@ export async function writeFileSafe(
   await fsp.writeFile(file, contents);
   return file;
 }
-
-export async function writeFilesSafe(
-  files: { file: string; contents: string }[]
-): Promise<string[]> {
-  return Promise.all(
-    files.map(({ file, contents }) => writeFileSafe(file, contents))
-  );
-}
-
-export async function createTemporaryDirectory(
-  baseDir: string
-): Promise<string> {
-  return fsp.mkdtemp(path.join(baseDir, "remix-"));
-}
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index c41db0e61..c2a5ef257 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -307,14 +307,14 @@ export async function readConfig(
   remixRoot?: string,
   serverMode = ServerMode.Production
 ): Promise<RemixConfig> {
-  if (!remixRoot) {
-    remixRoot = process.env.REMIX_ROOT || process.cwd();
-  }
-
   if (!isValidServerMode(serverMode)) {
     throw new Error(`Invalid server mode "${serverMode}"`);
   }
 
+  if (!remixRoot) {
+    remixRoot = process.env.REMIX_ROOT || process.cwd();
+  }
+
   let rootDirectory = path.resolve(remixRoot);
   let configFile = findConfig(rootDirectory, "remix.config");
 
@@ -450,15 +450,13 @@ export async function readConfig(
       appDirectory,
       appConfig.ignoredRouteFiles
     );
-    for (let key of Object.keys(conventionalRoutes)) {
-      let route = conventionalRoutes[key];
+    for (let route of Object.values(conventionalRoutes)) {
       routes[route.id] = { ...route, parentId: route.parentId || "root" };
     }
   }
   if (appConfig.routes) {
     let manualRoutes = await appConfig.routes(defineRoutes);
-    for (let key of Object.keys(manualRoutes)) {
-      let route = manualRoutes[key];
+    for (let route of Object.values(manualRoutes)) {
       routes[route.id] = { ...route, parentId: route.parentId || "root" };
     }
   }
diff --git a/packages/remix-dev/devServer/liveReload.ts b/packages/remix-dev/devServer/liveReload.ts
index 1ba637dc3..42817ba99 100644
--- a/packages/remix-dev/devServer/liveReload.ts
+++ b/packages/remix-dev/devServer/liveReload.ts
@@ -4,17 +4,22 @@ import path from "path";
 import prettyMs from "pretty-ms";
 import WebSocket from "ws";
 
-import { createChannel } from "../channel";
 import type { WatchOptions } from "../compiler";
 import { watch } from "../compiler";
 import type { RemixConfig } from "../config";
 
 const relativePath = (file: string) => path.relative(process.cwd(), file);
 
+let clean = (config: RemixConfig) => {
+  fse.emptyDirSync(config.assetsBuildDirectory);
+  fse.rmSync(config.serverBuildPath);
+};
+
 export async function liveReload(
   config: RemixConfig,
   options: WatchOptions = {}
 ) {
+  clean(config);
   let wss = new WebSocket.Server({ port: config.devServerPort });
   function broadcast(event: { type: string } & Record<string, unknown>) {
     setTimeout(() => {
@@ -36,6 +41,7 @@ export async function liveReload(
     mode: options.mode,
     onInitialBuild: options.onInitialBuild,
     onRebuildStart() {
+      clean(config);
       log("Rebuilding...");
     },
     onRebuildFinish(durationMs: number) {
@@ -53,14 +59,9 @@ export async function liveReload(
     },
   });
 
-  let channel = createChannel<void>();
-  exitHook(async () => {
-    // cleanup when process exits e.g. user hits CTRL-C
+  exitHook(() => clean(config));
+  return async () => {
     wss.close();
     await dispose();
-    fse.emptyDirSync(config.assetsBuildDirectory);
-    fse.rmSync(config.serverBuildPath);
-    channel.write();
-  });
-  return channel.read();
+  };
 }
diff --git a/packages/remix-dev/devServer/serve.ts b/packages/remix-dev/devServer/serve.ts
index b2e99f552..84ca83132 100644
--- a/packages/remix-dev/devServer/serve.ts
+++ b/packages/remix-dev/devServer/serve.ts
@@ -66,33 +66,30 @@ export async function serve(
     )
   );
 
+  let dispose = await liveReload(config);
   let server: Server | undefined;
-  try {
-    await liveReload(config, {
-      onInitialBuild: () => {
-        let onListen = () => {
-          let address =
-            process.env.HOST ||
-            Object.values(os.networkInterfaces())
-              .flat()
-              .find((ip) => String(ip?.family).includes("4") && !ip?.internal)
-              ?.address;
-
-          if (!address) {
-            console.log(`Remix App Server started at http://localhost:${port}`);
-          } else {
-            console.log(
-              `Remix App Server started at http://localhost:${port} (http://${address}:${port})`
-            );
-          }
-        };
+  let onListen = () => {
+    let address =
+      process.env.HOST ||
+      Object.values(os.networkInterfaces())
+        .flat()
+        .find((ip) => String(ip?.family).includes("4") && !ip?.internal)
+        ?.address;
 
-        server = process.env.HOST
-          ? app.listen(port, process.env.HOST, onListen)
-          : app.listen(port, onListen);
-      },
-    });
+    if (!address) {
+      console.log(`Remix App Server started at http://localhost:${port}`);
+    } else {
+      console.log(
+        `Remix App Server started at http://localhost:${port} (http://${address}:${port})`
+      );
+    }
+  };
+  try {
+    server = process.env.HOST
+      ? app.listen(port, process.env.HOST, onListen)
+      : app.listen(port, onListen);
   } finally {
+    dispose();
     server?.close();
   }
 }
diff --git a/packages/remix-dev/devServer2.ts b/packages/remix-dev/devServer2.ts
index 3ec2f7f13..02ec0ee2f 100644
--- a/packages/remix-dev/devServer2.ts
+++ b/packages/remix-dev/devServer2.ts
@@ -1,3 +1,5 @@
+import exitHook from "exit-hook";
+import fs from "fs-extra";
 import getPort, { makeRange } from "get-port";
 import os from "os";
 import path from "node:path";
@@ -16,6 +18,11 @@ let relativePath = (file: string) => path.relative(process.cwd(), file);
 
 let sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
 
+let clean = (config: RemixConfig) => {
+  fs.emptyDirSync(config.relativeAssetsBuildDirectory);
+  fs.removeSync(config.serverBuildPath);
+};
+
 let getHost = () =>
   process.env.HOST ??
   Object.values(os.networkInterfaces())
@@ -78,6 +85,7 @@ export let serve = async (
   config: RemixConfig,
   flags: { port?: number; appServerPort?: number } = {}
 ) => {
+  clean(config);
   await loadEnv(config.rootDirectory);
 
   let dev = resolveDev(config.future.unstable_dev, flags);
@@ -105,7 +113,10 @@ export let serve = async (
     mode: "development",
     liveReloadPort: port,
     onInitialBuild: (durationMs) => info(`Built in ${prettyMs(durationMs)}`),
-    onRebuildStart: () => socket.log("Rebuilding..."),
+    onRebuildStart: () => {
+      clean(config);
+      socket.log("Rebuilding...");
+    },
     onRebuildFinish: async (durationMs, assetsManifest) => {
       if (!assetsManifest) return;
       socket.log(`Rebuilt in ${prettyMs(durationMs)}`);
@@ -122,8 +133,8 @@ export let serve = async (
     onFileDeleted: (file) => socket.log(`File deleted: ${relativePath(file)}`),
   });
 
-  // TODO exit hook: clean up assetsBuildDirectory and serverBuildPath?
-
+  // clean up build directories when dev server exits
+  exitHook(() => clean(config));
   return async () => {
     await dispose();
     socket.close();
diff --git a/packages/remix-eslint-config/rules/packageExports.js b/packages/remix-eslint-config/rules/packageExports.js
index bbef0084c..a3c02efe3 100644
--- a/packages/remix-eslint-config/rules/packageExports.js
+++ b/packages/remix-eslint-config/rules/packageExports.js
@@ -66,7 +66,7 @@ const architectSpecificExports = {
 };
 
 const cloudflareSpecificExports = {
-  value: ["createCloudflareKVSessionStorage"],
+  value: ["createCloudflareKVSessionStorage", "createWorkersKVSessionStorage"],
   type: [],
 };
 
diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts
index 73b471137..86535abfd 100644
--- a/packages/remix-node/index.ts
+++ b/packages/remix-node/index.ts
@@ -42,7 +42,6 @@ export {
   isCookie,
   isSession,
   json,
-  JsonFunction,
   MaxPartSizeExceededError,
   redirect,
   unstable_composeUploadHandlers,
@@ -69,6 +68,7 @@ export type {
   HtmlLinkDescriptor,
   HtmlMetaDescriptor,
   V2_HtmlMetaDescriptor,
+  JsonFunction,
   LinkDescriptor,
   LinksFunction,
   LoaderArgs,
diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx
index 31df2f30b..8075d402f 100644
--- a/packages/remix-react/components.tsx
+++ b/packages/remix-react/components.tsx
@@ -1536,9 +1536,11 @@ export const LiveReload =
     ? () => null
     : function LiveReload({
         port = Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002),
+        timeoutMs = 1000,
         nonce = undefined,
       }: {
         port?: number;
+        timeoutMs?: number;
         /**
          * @deprecated this property is no longer relevant.
          */
@@ -1582,7 +1584,7 @@ export const LiveReload =
                           remixLiveReloadConnect({
                             onOpen: () => window.location.reload(),
                           }),
-                        1000
+                      ${String(timeoutMs)}
                       );
                     }
                   };
diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx
index 8387150d9..e0f349836 100644
--- a/packages/remix-react/index.tsx
+++ b/packages/remix-react/index.tsx
@@ -70,6 +70,7 @@ export { useCatch } from "./errorBoundaries";
 
 export type { HtmlLinkDescriptor } from "./links";
 export type {
+  CatchBoundaryComponent,
   HtmlMetaDescriptor,
   RouteModules as UNSAFE_RouteModules,
   ShouldReloadFunction,
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index 881088ab3..f943add14 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -18,6 +18,7 @@
   "dependencies": {
     "@remix-run/router": "1.3.0",
     "@types/cookie": "^0.4.0",
+    "@types/react": "^18.0.15",
     "@web3-storage/multipart-parser": "^1.0.0",
     "cookie": "^0.4.1",
     "set-cookie-parser": "^2.4.8",
@@ -25,13 +26,7 @@
   },
   "devDependencies": {
     "@remix-run/web-file": "^3.0.2",
-    "@types/set-cookie-parser": "^2.4.1",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
-  },
-  "peerDependencies": {
-    "react": ">=16.8",
-    "react-dom": ">=16.8"
+    "@types/set-cookie-parser": "^2.4.1"
   },
   "engines": {
     "node": ">=14"
diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts
index 933a2390e..380b2549d 100644
--- a/packages/remix-server-runtime/routeModules.ts
+++ b/packages/remix-server-runtime/routeModules.ts
@@ -1,5 +1,5 @@
-import type { ComponentType } from "react";
 import type { Location, Params } from "@remix-run/router";
+import type { ComponentType } from "react";
 
 import type { AppLoadContext, AppData } from "./data";
 import type { LinkDescriptor } from "./links";
@@ -37,7 +37,7 @@ export interface ActionFunction {
 /**
  * A React component that is rendered when the server throws a Response.
  */
-export type CatchBoundaryComponent = ComponentType<{}>;
+export type CatchBoundaryComponent = ComponentType;
 
 /**
  * A React component that is rendered when there is an error on a route.
diff --git a/rollup.utils.js b/rollup.utils.js
index 7e415b3d7..d8c352620 100644
--- a/rollup.utils.js
+++ b/rollup.utils.js
@@ -267,6 +267,7 @@ function getMagicExports(packageName) {
         "createCookieSessionStorage",
         "createMemorySessionStorage",
         "createSessionStorage",
+        "createWorkersKVSessionStorage",
       ],
     },
     "@remix-run/node": {


Test patch:
diff --git a/integration/cf-compiler-test.ts b/integration/cf-compiler-test.ts
index a1d058e98..74c8ae529 100644
--- a/integration/cf-compiler-test.ts
+++ b/integration/cf-compiler-test.ts
@@ -155,6 +155,7 @@ test.describe("cloudflare compiler", () => {
       "createMemorySessionStorage",
       "createSessionStorage",
       "createSession",
+      "createWorkersKVSessionStorage",
       "isCookie",
       "isSession",
       "json",
diff --git a/packages/remix-dev/__tests__/transform-replaceRemixMagicImports-test.ts b/packages/remix-dev/__tests__/transform-replaceRemixMagicImports-test.ts
index e508c792d..2afa427c6 100644
--- a/packages/remix-dev/__tests__/transform-replaceRemixMagicImports-test.ts
+++ b/packages/remix-dev/__tests__/transform-replaceRemixMagicImports-test.ts
@@ -61,7 +61,7 @@ it("replaces multi-kind, multi-specifier imports", async () => {
 
 it("replaces runtime-specific and adapter-specific imports", async () => {
   let code = [
-    'import { json, createCloudflareKVSessionStorage, createRequestHandler, createPagesFunctionHandler, Form } from "remix"',
+    'import { json, createWorkersKVSessionStorage, createRequestHandler, createPagesFunctionHandler, Form } from "remix"',
     'import type { ActionFunction, GetLoadContextFunction, createPagesFunctionHandlerParams, LinkProps } from "remix"',
   ].join("\n");
   let transform = replaceRemixMagicImports({
@@ -71,7 +71,7 @@ it("replaces runtime-specific and adapter-specific imports", async () => {
   let result = eol.normalize(transform(code, "fake.tsx"));
   expect(result).toBe(
     [
-      'import { type ActionFunction, createCloudflareKVSessionStorage, json } from "@remix-run/cloudflare";',
+      'import { type ActionFunction, createWorkersKVSessionStorage, json } from "@remix-run/cloudflare";',
       "", // recast adds a newline here https://github.com/benjamn/recast/issues/39
       "import {",
       "  type GetLoadContextFunction,",
diff --git a/packages/remix-react/__tests__/components-test.tsx b/packages/remix-react/__tests__/components-test.tsx
index d3d76b99a..f9dcb3309 100644
--- a/packages/remix-react/__tests__/components-test.tsx
+++ b/packages/remix-react/__tests__/components-test.tsx
@@ -65,6 +65,14 @@ describe("<LiveReload />", () => {
         "let port = (window.__remixContext.dev && window.__remixContext.dev.liveReloadPort) || 1234;"
       );
     });
+
+    it("timeout of reload is set to 200ms", () => {
+      LiveReload = require("../components").LiveReload;
+      let { container } = render(<LiveReload timeoutMs={200} />);
+      expect(container.querySelector("script")).toHaveTextContent(
+        "setTimeout( () => remixLiveReloadConnect({ onOpen: () => window.location.reload(), }), 200 );"
+      );
+    });
   });
 });