diff --git a/.changeset/old-cameras-lay.md b/.changeset/old-cameras-lay.md
new file mode 100644
index 00000000000..9c321b36ed7
--- /dev/null
+++ b/.changeset/old-cameras-lay.md
@@ -0,0 +1,22 @@
+---
+"remix": patch
+"@remix-run/cloudflare": patch
+"@remix-run/deno": patch
+"@remix-run/node": patch
+"@remix-run/react": patch
+"@remix-run/serve": patch
+"@remix-run/server-runtime": patch
+"@remix-run/testing": patch
+---
+
+Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from loaders by passing promises instead of resolved values. This has been refered to as "promise over the wire".
+
+Informational Resources:
+- https://gist.github.com/jacob-ebey/9bde9546c1aafaa6bc8c242054b1be26
+- https://github.com/remix-run/remix/blob/main/decisions/0004-streaming-apis.md
+
+Documentation Resources (better docs specific to remix are in the works):
+- https://reactrouter.com/en/main/utils/defer
+- https://reactrouter.com/en/main/components/await
+- https://reactrouter.com/en/main/hooks/use-async-value
+- https://reactrouter.com/en/main/hooks/use-async-error
diff --git a/integration/defer-loader-test.ts b/integration/defer-loader-test.ts
new file mode 100644
index 00000000000..a8a292f9f34
--- /dev/null
+++ b/integration/defer-loader-test.ts
@@ -0,0 +1,93 @@
+import { test, expect } from "@playwright/test";
+
+import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
+import type { Fixture, AppFixture } from "./helpers/create-fixture";
+import { PlaywrightFixture } from "./helpers/playwright-fixture";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/index.jsx": js`
+ import { useLoaderData, Link } from "@remix-run/react";
+ export default function Index() {
+ return (
+
+ Redirect
+ Direct Promise Access
+
+ )
+ }
+ `,
+
+ "app/routes/redirect.jsx": js`
+ import { defer } from "@remix-run/node";
+ export function loader() {
+ return defer({food: "pizza"}, { status: 301, headers: { Location: "/?redirected" } });
+ }
+ export default function Redirect() {return null;}
+ `,
+
+ "app/routes/direct-promise-access.jsx": js`
+ import * as React from "react";
+ import { defer } from "@remix-run/node";
+ import { useLoaderData, Link, Await } from "@remix-run/react";
+ export function loader() {
+ return defer({
+ bar: new Promise(async (resolve, reject) => {
+ resolve("hamburger");
+ }),
+ });
+ }
+ let count = 0;
+ export default function Index() {
+ let {bar} = useLoaderData();
+ React.useEffect(() => {
+ let aborted = false;
+ bar.then((data) => {
+ if (aborted) return;
+ document.getElementById("content").innerHTML = data + " " + (++count);
+ document.getElementById("content").setAttribute("data-done", "");
+ });
+ return () => {
+ aborted = true;
+ };
+ }, [bar]);
+ return (
+
+ Waiting for client hydration....
+
+ )
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(async () => appFixture.close());
+
+test("deferred response can redirect on document request", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect");
+ await page.waitForURL(/\?redirected/);
+});
+
+test("deferred response can redirect on transition", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/redirect");
+ await page.waitForURL(/\?redirected/);
+});
+
+test("can directly access result from deferred promise on document request", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/direct-promise-access");
+ let element = await page.waitForSelector("[data-done]");
+ expect(await element.innerText()).toMatch("hamburger 1");
+});
diff --git a/integration/defer-test.ts b/integration/defer-test.ts
new file mode 100644
index 00000000000..bbb85be363d
--- /dev/null
+++ b/integration/defer-test.ts
@@ -0,0 +1,1272 @@
+import { test, expect } from "@playwright/test";
+import type { ConsoleMessage, Page } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture";
+import type { Fixture, AppFixture } from "./helpers/create-fixture";
+import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
+
+const ROOT_ID = "ROOT_ID";
+const INDEX_ID = "INDEX_ID";
+const DEFERRED_ID = "DEFERRED_ID";
+const RESOLVED_DEFERRED_ID = "RESOLVED_DEFERRED_ID";
+const FALLBACK_ID = "FALLBACK_ID";
+const ERROR_ID = "ERROR_ID";
+const ERROR_BOUNDARY_ID = "ERROR_BOUNDARY_ID";
+const MANUAL_RESOLVED_ID = "MANUAL_RESOLVED_ID";
+const MANUAL_FALLBACK_ID = "MANUAL_FALLBACK_ID";
+const MANUAL_ERROR_ID = "MANUAL_ERROR_ID";
+
+declare global {
+ // eslint-disable-next-line prefer-let/prefer-let
+ var __deferredManualResolveCache: {
+ nextId: number;
+ deferreds: Record<
+ string,
+ { resolve: (value: any) => void; reject: (error: Error) => void }
+ >;
+ };
+}
+
+test.describe("non-aborted", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ ////////////////////////////////////////////////////////////////////////////
+ // 💿 Next, add files to this object, just like files in a real app,
+ // `createFixture` will make an app and run your tests against it.
+ ////////////////////////////////////////////////////////////////////////////
+ files: {
+ "app/components/counter.tsx": js`
+ import { useState } from "react";
+
+ export default function Counter({ id }) {
+ let [count, setCount] = useState(0);
+ return (
+
+
setCount((c) => c+1)}>Increment
+
{count}
+
+ )
+ }
+ `,
+ "app/components/interactive.tsx": js`
+ import { useEffect, useState } from "react";
+
+ export default function Interactive() {
+ let [interactive, setInteractive] = useState(false);
+ useEffect(() => {
+ setInteractive(true);
+ }, []);
+ return interactive ? (
+
+ ) : null;
+ }
+ `,
+ "app/root.tsx": js`
+ import { defer } from "@remix-run/node";
+ import { Links, Meta, Outlet, Scripts, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+ import Interactive from "~/components/interactive";
+
+ export const meta: MetaFunction = () => ({
+ charset: "utf-8",
+ title: "New Remix App",
+ viewport: "width=device-width,initial-scale=1",
+ });
+
+ export const loader = () => defer({
+ id: "${ROOT_ID}",
+ });
+
+ export default function Root() {
+ let { id } = useLoaderData();
+ return (
+
+
+
+
+
+
+
+
+ {/* Send arbitrary data so safari renders the initial shell before
+ the document finishes downloading. */}
+ {Array(10000).fill(null).map((_, i)=>YOOOOOOOOOO {i}
)}
+
+
+ );
+ }
+ `,
+
+ "app/routes/index.tsx": js`
+ import { defer } from "@remix-run/node";
+ import { Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ id: "${INDEX_ID}",
+ });
+ }
+
+ export default function Index() {
+ let { id } = useLoaderData();
+ return (
+
+
{id}
+
+
+
+ deferred-script-resolved
+ deferred-script-unresolved
+ deferred-script-rejected
+ deferred-script-unrejected
+ deferred-script-rejected-no-error-element
+ deferred-script-unrejected-no-error-element
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-noscript-resolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-noscript-unresolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-resolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unresolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-rejected.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+
+ error
+
+
+ }
+ children={(resolvedDeferredId) => (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unrejected.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (_, reject) => setTimeout(() => {
+ reject(new Error("${RESOLVED_DEFERRED_ID}"));
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+
+ error
+
+
+ }
+ children={(resolvedDeferredId) => (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-rejected-no-error-element.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ return (
+
+ error
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unrejected-no-error-element.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (_, reject) => setTimeout(() => {
+ reject(new Error("${RESOLVED_DEFERRED_ID}"));
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ return (
+
+ error
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-manual-resolve.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ global.__deferredManualResolveCache = global.__deferredManualResolveCache || {
+ nextId: 1,
+ deferreds: {},
+ };
+
+ let id = "" + global.__deferredManualResolveCache.nextId++;
+ let promise = new Promise((resolve, reject) => {
+ global.__deferredManualResolveCache.deferreds[id] = { resolve, reject };
+ });
+
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ id,
+ manualValue: promise,
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId, id, manualValue } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+ )}
+ />
+
+ manual fallback}>
+
+ error
+
+
+ }
+ children={(value) => (
+
+
{JSON.stringify(value)}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using puppeteer.
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("works with critical JSON like data", async ({ page }) => {
+ let response = await fixture.requestDocument("/");
+ let html = await response.text();
+ let criticalHTML = html.slice(0, html.indexOf("