Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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