Skip to content

Commit

Permalink
Minor client data edge case fixes (#8253)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Dec 8, 2023
1 parent 96eb33b commit 4fc551a
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-falcons-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/react": patch
---

[REMOVE] Fix a few edge cases with client data (throw if server handler doesnt exist, and only return initialData on first loader call)
323 changes: 319 additions & 4 deletions integration/client-data-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,6 @@ test.describe("Client Data", () => {
childClientLoader: false,
childClientLoaderHydrate: false,
}),
// Blow away parent.child.tsx with our own deferred version
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { defer, json } from '@remix-run/node'
Expand Down Expand Up @@ -415,7 +414,6 @@ test.describe("Client Data", () => {
childClientLoader: false,
childClientLoaderHydrate: false,
}),
// Blow away parent.child.tsx with our own version
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { json } from '@remix-run/node';
Expand Down Expand Up @@ -470,7 +468,6 @@ test.describe("Client Data", () => {
childClientLoader: false,
childClientLoaderHydrate: false,
}),
// Blow away parent.child.tsx with our own version without a server loader
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { useLoaderData } from '@remix-run/react';
Expand Down Expand Up @@ -514,7 +511,6 @@ test.describe("Client Data", () => {
childClientLoader: false,
childClientLoaderHydrate: false,
}),
// Blow away parent.child.tsx with our own version without a server loader
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { useLoaderData } from '@remix-run/react';
Expand Down Expand Up @@ -545,6 +541,189 @@ test.describe("Client Data", () => {
html = await app.getHtml("main");
expect(html).toMatch("Loader Data (clientLoader only)");
});

test("throws a 400 if you call serverLoader without a server loader", async ({
page,
}) => {
appFixture = await createAppFixture(
await createFixture({
files: {
...getFiles({
parentClientLoader: false,
parentClientLoaderHydrate: false,
childClientLoader: false,
childClientLoaderHydrate: false,
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { useLoaderData, useRouteError } from '@remix-run/react';
export async function clientLoader({ serverLoader }) {
return await serverLoader();
}
export default function Component() {
return <p>Child</p>;
}
export function HydrateFallback() {
return <p>Loading...</p>;
}
export function ErrorBoundary() {
let error = useRouteError();
return <p id="child-error">{error.status} {error.data}</p>;
}
`,
},
})
);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/parent/child");
await page.waitForSelector("#child-error");
let html = await app.getHtml("#child-error");
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
"400 Error: You are trying to call serverLoader() on a route that does " +
'not have a server loader (routeId: "routes/parent.child")'
);
});

test("initial hydration data check functions properly", async ({
page,
}) => {
appFixture = await createAppFixture(
await createFixture({
files: {
...getFiles({
parentClientLoader: false,
parentClientLoaderHydrate: false,
childClientLoader: false,
childClientLoaderHydrate: false,
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { json } from '@remix-run/node';
import { useLoaderData, useRevalidator } from '@remix-run/react';
let isFirstCall = true;
export async function loader({ serverLoader }) {
if (isFirstCall) {
isFirstCall = false
return json({
message: "Child Server Loader Data (1)",
});
}
return json({
message: "Child Server Loader Data (2+)",
});
}
export async function clientLoader({ serverLoader }) {
await new Promise(r => setTimeout(r, 100));
let serverData = await serverLoader();
return {
message: serverData.message + " (mutated by client)",
};
}
clientLoader.hydrate=true;
export default function Component() {
let data = useLoaderData();
let revalidator = useRevalidator();
return (
<>
<p id="child-data">{data.message}</p>
<button onClick={() => revalidator.revalidate()}>Revalidate</button>
</>
);
}
export function HydrateFallback() {
return <p>Loading...</p>
}
`,
},
})
);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/parent/child");
await page.waitForSelector("#child-data");
let html = await app.getHtml();
expect(html).toMatch("Child Server Loader Data (1) (mutated by client)");
app.clickElement("button");
await page.waitForSelector(':has-text("Child Server Loader Data (2+)")');
html = await app.getHtml("main");
expect(html).toMatch("Child Server Loader Data (2+) (mutated by client)");
});

test("initial hydration data check functions properly even if serverLoader isn't called on hydration", async ({
page,
}) => {
appFixture = await createAppFixture(
await createFixture({
files: {
...getFiles({
parentClientLoader: false,
parentClientLoaderHydrate: false,
childClientLoader: false,
childClientLoaderHydrate: false,
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { json } from '@remix-run/node';
import { useLoaderData, useRevalidator } from '@remix-run/react';
let isFirstCall = true;
export async function loader({ serverLoader }) {
if (isFirstCall) {
isFirstCall = false
return json({
message: "Child Server Loader Data (1)",
});
}
return json({
message: "Child Server Loader Data (2+)",
});
}
let isFirstClientCall = true;
export async function clientLoader({ serverLoader }) {
await new Promise(r => setTimeout(r, 100));
if (isFirstClientCall) {
isFirstClientCall = false;
// First time through - don't even call serverLoader
return {
message: "Child Client Loader Data",
};
}
// Only call the serverLoader on subsequent calls and this
// should *not* return us the initialData any longer
let serverData = await serverLoader();
return {
message: serverData.message + " (mutated by client)",
};
}
clientLoader.hydrate=true;
export default function Component() {
let data = useLoaderData();
let revalidator = useRevalidator();
return (
<>
<p id="child-data">{data.message}</p>
<button onClick={() => revalidator.revalidate()}>Revalidate</button>
</>
);
}
export function HydrateFallback() {
return <p>Loading...</p>
}
`,
},
})
);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/parent/child");
await page.waitForSelector("#child-data");
let html = await app.getHtml();
expect(html).toMatch("Child Client Loader Data");
app.clickElement("button");
await page.waitForSelector(':has-text("Child Server Loader Data (2+)")');
html = await app.getHtml("main");
expect(html).toMatch("Child Server Loader Data (2+) (mutated by client)");
});
});

test.describe("clientLoader - lazy route module", () => {
Expand Down Expand Up @@ -632,6 +811,50 @@ test.describe("Client Data", () => {
expect(html).toMatch("Parent Server Loader (mutated by client)");
expect(html).toMatch("Child Server Loader (mutated by client");
});

test("throws a 400 if you call serverLoader without a server loader", async ({
page,
}) => {
appFixture = await createAppFixture(
await createFixture({
files: {
...getFiles({
parentClientLoader: false,
parentClientLoaderHydrate: false,
childClientLoader: false,
childClientLoaderHydrate: false,
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { useLoaderData, useRouteError } from '@remix-run/react';
export async function clientLoader({ serverLoader }) {
return await serverLoader();
}
export default function Component() {
return <p>Child</p>;
}
export function HydrateFallback() {
return <p>Loading...</p>;
}
export function ErrorBoundary() {
let error = useRouteError();
return <p id="child-error">{error.status} {error.data}</p>;
}
`,
},
})
);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/");
await app.clickLink("/parent/child");
await page.waitForSelector("#child-error");
let html = await app.getHtml("#child-error");
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
"400 Error: You are trying to call serverLoader() on a route that does " +
'not have a server loader (routeId: "routes/parent.child")'
);
});
});

test.describe("clientAction - critical route module", () => {
Expand Down Expand Up @@ -796,6 +1019,51 @@ test.describe("Client Data", () => {
expect(html).toMatch("Child Server Loader (mutated by client)");
expect(html).toMatch("Child Server Action (mutated by client)");
});

test("throws a 400 if you call serverAction without a server action", async ({
page,
}) => {
appFixture = await createAppFixture(
await createFixture({
files: {
...getFiles({
parentClientLoader: false,
parentClientLoaderHydrate: false,
childClientLoader: false,
childClientLoaderHydrate: false,
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { json } from '@remix-run/node';
import { Form, useRouteError } from '@remix-run/react';
export async function clientAction({ serverAction }) {
return await serverAction();
}
export default function Component() {
return (
<Form method="post">
<button type="submit">Submit</button>
</Form>
);
}
export function ErrorBoundary() {
let error = useRouteError();
return <p id="child-error">{error.status} {error.data}</p>;
}
`,
},
})
);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent/child");
app.clickSubmitButton("/parent/child");
await page.waitForSelector("#child-error");
let html = await app.getHtml("#child-error");
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
"400 Error: You are trying to call serverAction() on a route that does " +
'not have a server action (routeId: "routes/parent.child")'
);
});
});

test.describe("clientAction - lazy route module", () => {
Expand Down Expand Up @@ -968,5 +1236,52 @@ test.describe("Client Data", () => {
expect(html).toMatch("Child Server Loader (mutated by client)");
expect(html).toMatch("Child Server Action (mutated by client)");
});

test("throws a 400 if you call serverAction without a server action", async ({
page,
}) => {
appFixture = await createAppFixture(
await createFixture({
files: {
...getFiles({
parentClientLoader: false,
parentClientLoaderHydrate: false,
childClientLoader: false,
childClientLoaderHydrate: false,
}),
"app/routes/parent.child.tsx": js`
import * as React from 'react';
import { json } from '@remix-run/node';
import { Form, useRouteError } from '@remix-run/react';
export async function clientAction({ serverAction }) {
return await serverAction();
}
export default function Component() {
return (
<Form method="post">
<button type="submit">Submit</button>
</Form>
);
}
export function ErrorBoundary() {
let error = useRouteError();
return <p id="child-error">{error.status} {error.data}</p>;
}
`,
},
})
);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await app.goto("/parent/child");
await page.waitForSelector("form");
app.clickSubmitButton("/parent/child");
await page.waitForSelector("#child-error");
let html = await app.getHtml("#child-error");
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
"400 Error: You are trying to call serverAction() on a route that does " +
'not have a server action (routeId: "routes/parent.child")'
);
});
});
});
Loading

0 comments on commit 4fc551a

Please sign in to comment.