Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

defer - for real this time #4920

Merged
merged 9 commits into from
Jan 13, 2023
22 changes: 22 additions & 0 deletions .changeset/old-cameras-lay.md
Original file line number Diff line number Diff line change
@@ -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
93 changes: 93 additions & 0 deletions integration/defer-loader-test.ts
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<Link to="/redirect">Redirect</Link>
<Link to="/direct-promise-access">Direct Promise Access</Link>
</div>
)
}
`,

"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 (
<div id="content">
Waiting for client hydration....
</div>
)
}
`,
},
});

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");
});
Loading