Skip to content

Commit

Permalink
RRR Step 3 - Hydration (#4703)
Browse files Browse the repository at this point in the history
Co-authored-by: Jacob Ebey <jacob.ebey@live.com>
  • Loading branch information
brophdawg11 and jacob-ebey committed Dec 14, 2022
1 parent a340693 commit 5d472de
Show file tree
Hide file tree
Showing 15 changed files with 744 additions and 361 deletions.
62 changes: 37 additions & 25 deletions integration/request-test.ts
Expand Up @@ -13,23 +13,27 @@ test.beforeAll(async () => {
"app/routes/index.jsx": js`
import { json } from "@remix-run/node";
import { Form, useLoaderData, useActionData } from "@remix-run/react";
export async function loader({ request }) {
let text = (await request.text());
async function requestToJson(request) {
let body = null;
if (request.body) {
let fd = await request.formData();
body = Object.fromEntries(fd.entries());
}
return json({
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
text,
body,
});
}
export async function action({ request }) {
let text = (await request.text());
return json({
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
text,
});
export async function loader({ request }) {
return requestToJson(request);
}
export function action({ request }) {
return requestToJson(request);
}
export default function Index() {
let loaderData = useLoaderData();
Expand All @@ -42,16 +46,24 @@ test.beforeAll(async () => {
Set Cookie
</button>
<Form method="get" reloadDocument>
<button type="submit" id="submit-get-ssr" name="type" value="ssr" />
<button type="submit" id="submit-get-ssr" name="type" value="ssr">
SSR GET
</button>
</Form>
<Form method="get">
<button type="submit" id="submit-get-csr" name="type" value="csr" />
<button type="submit" id="submit-get-csr" name="type" value="csr">
CSR GET
</button>
</Form>
<Form method="post" reloadDocument>
<button type="submit" id="submit-post-ssr" name="type" value="ssr" />
<button type="submit" id="submit-post-ssr" name="type" value="ssr">
SSR POST
</button>
</Form>
<Form method="post">
<button type="submit" id="submit-post-csr" name="type" value="csr" />
<button type="submit" id="submit-post-csr" name="type" value="csr">
CSR POST
</button>
</Form>
<pre id="loader-data">{JSON.stringify(loaderData)}</pre>
{actionData ?
Expand All @@ -78,15 +90,15 @@ test("loader request on SSR GET requests", async ({ page }) => {
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);

await app.clickElement("#submit-get-ssr");

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/\?type=ssr$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);
});

test("loader request on CSR GET requests", async ({ page }) => {
Expand All @@ -98,15 +110,15 @@ test("loader request on CSR GET requests", async ({ page }) => {
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);

await app.clickElement("#submit-get-csr");

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/\?type=csr$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);
});

test("action + loader requests SSR POST requests", async ({ page }) => {
Expand All @@ -118,21 +130,21 @@ test("action + loader requests SSR POST requests", async ({ page }) => {
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);

await app.clickElement("#submit-post-ssr");

let actionData = JSON.parse(await page.locator("#action-data").innerHTML());
expect(actionData.method).toEqual("POST");
expect(actionData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(actionData.headers.cookie).toEqual("cookie=nomnom");
expect(actionData.text).toEqual("type=ssr");
expect(actionData.body).toEqual({ type: "ssr" });

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);
});

test("action + loader requests on CSR POST requests", async ({ page }) => {
Expand All @@ -144,19 +156,19 @@ test("action + loader requests on CSR POST requests", async ({ page }) => {
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);

await app.clickElement("#submit-post-csr");

let actionData = JSON.parse(await page.locator("#action-data").innerHTML());
expect(actionData.method).toEqual("POST");
expect(actionData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(actionData.headers.cookie).toEqual("cookie=nomnom");
expect(actionData.text).toEqual("type=csr");
expect(actionData.body).toEqual({ type: "csr" });

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
expect(loaderData.body).toEqual(null);
});
35 changes: 23 additions & 12 deletions integration/transition-state-test.ts
Expand Up @@ -31,6 +31,7 @@ test.describe("rendering", () => {
import { Outlet, Scripts, useTransition } from "@remix-run/react";
export default function() {
const transition = useTransition();
const transitionsRef = useRef();
const transitions = useMemo(() => {
const savedTransitions = transitionsRef.current || [];
Expand All @@ -41,6 +42,7 @@ test.describe("rendering", () => {
return (
<html lang="en">
<head><title>Test</title></head>
<body>
<Outlet />
{transition.state != "idle" && (
Expand Down Expand Up @@ -254,9 +256,11 @@ test.describe("rendering", () => {
search: "?redirected",
hash: "",
state: {
isRedirect: true,
setCookie: false,
type: "loader",
_isRedirect: true,
// These were private API for transition manager that are no longer
// needed with the new router so OK to disappear
// setCookie: false,
// type: "loader",
},
key: expect.any(String),
},
Expand Down Expand Up @@ -342,9 +346,11 @@ test.describe("rendering", () => {
search: "?redirected",
hash: "",
state: {
isRedirect: true,
setCookie: false,
type: "loaderSubmission",
_isRedirect: true,
// These were private API for transition manager that are no longer
// needed with the new router so OK to disappear
// setCookie: false,
// type: "loader",
},
key: expect.any(String),
},
Expand Down Expand Up @@ -452,9 +458,11 @@ test.describe("rendering", () => {
search: "?redirected",
hash: "",
state: {
isRedirect: true,
setCookie: false,
type: "action",
_isRedirect: true,
// These were private API for transition manager that are no longer
// needed with the new router so OK to disappear
// setCookie: false,
// type: "loader",
},
key: expect.any(String),
},
Expand Down Expand Up @@ -491,9 +499,12 @@ test.describe("rendering", () => {
search: "?redirected",
hash: "",
state: {
isRedirect: true,
setCookie: false,
type: "fetchAction",
_isRedirect: true,
_isFetchActionRedirect: true,
// These were private API for transition manager that are no longer
// needed with the new router so OK to disappear
// setCookie: false,
// type: "loader",
},
key: expect.any(String),
},
Expand Down
11 changes: 10 additions & 1 deletion packages/remix-eslint-config/rules/core.js
Expand Up @@ -89,7 +89,16 @@ module.exports = {
"no-new-object": WARN,
"no-octal": WARN,
"no-redeclare": ERROR,
"no-restricted-imports": [WARN, ...replaceRemixImportsOptions],
"no-restricted-imports": [
WARN,
...replaceRemixImportsOptions,
{
importNames: ["useTransition"],
message:
"useTransition is deprecated in favor of useNavigation as of v1.9.0.",
name: "@remix-run/react",
},
],
"no-script-url": WARN,
"no-self-assign": WARN,
"no-self-compare": WARN,
Expand Down
71 changes: 34 additions & 37 deletions packages/remix-react/browser.tsx
@@ -1,64 +1,61 @@
import type { BrowserHistory, Action, Location } from "@remix-run/router";
import { createBrowserHistory } from "@remix-run/router";
import type { HydrationState, Router } from "@remix-run/router";
import type { ReactElement } from "react";
import * as React from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

import { RemixEntry } from "./components";
import type { EntryContext } from "./entry";
import { RemixContext } from "./components";
import type { EntryContext, FutureConfig } from "./entry";
import { deserializeErrors } from "./errors";
import type { RouteModules } from "./routeModules";
import { createClientRoutes } from "./routes";

/* eslint-disable prefer-let/prefer-let */
declare global {
var __remixContext: EntryContext;
var __remixContext: {
state: HydrationState;
future: FutureConfig;
};
var __remixRouteModules: RouteModules;
var __remixManifest: EntryContext["manifest"];
}
/* eslint-enable prefer-let/prefer-let */

export interface RemixBrowserProps {}

interface Update {
action: Action;
location: Location;
}
let router: Router;

/**
* The entry point for a Remix app when it is rendered in the browser (in
* `app/entry.client.js`). This component is used by React to hydrate the HTML
* that was received from the server.
*/
export function RemixBrowser(_props: RemixBrowserProps): ReactElement {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory({ window, v5Compat: true });
}

let history = historyRef.current;
let [state, dispatch] = React.useReducer(
(_: Update, update: Update) => update,
{
action: history.action,
location: history.location,
if (!router) {
let routes = createClientRoutes(
window.__remixManifest.routes,
window.__remixRouteModules
);

let hydrationData = window.__remixContext.state;
if (hydrationData && hydrationData.errors) {
hydrationData = {
...hydrationData,
errors: deserializeErrors(hydrationData.errors),
};
}
);

React.useLayoutEffect(() => history.listen(dispatch), [history]);

let entryContext = window.__remixContext;
entryContext.manifest = window.__remixManifest;
entryContext.routeModules = window.__remixRouteModules;
// In the browser, we don't need this because a) in the case of loader
// errors we already know the order and b) in the case of render errors
// React knows the order and handles error boundaries normally.
entryContext.appState.trackBoundaries = false;
entryContext.appState.trackCatchBoundaries = false;
router = createBrowserRouter(routes, { hydrationData });
}

return (
<RemixEntry
context={entryContext}
action={state.action}
location={state.location}
navigator={history}
/>
<RemixContext.Provider
value={{
manifest: window.__remixManifest,
routeModules: window.__remixRouteModules,
future: window.__remixContext.future,
}}
>
<RouterProvider router={router} fallbackElement={null} />
</RemixContext.Provider>
);
}

0 comments on commit 5d472de

Please sign in to comment.