Skip to content

Commit

Permalink
Wrap resource route raw object return in json for v2 back compat
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Apr 30, 2024
1 parent 75321d3 commit 56e0a3f
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-spoons-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/server-runtime": patch
---

Automatically wrap resource route naked object returns in `json()` for back-compat in v2 (and log deprecation warning)
25 changes: 25 additions & 0 deletions docs/guides/single-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,30 @@ And then when `c` calls `serverLoader`, it'll make it's own call for just the `c
GET /a/b/c.data?_routes=routes/c
```

### Resource Routes

Because of the new streaming format used by Single Fetch, raw JavaScript objects returned from `loader` and `action` functions are no longer automatically converted to `Response` instances via the `json()` utility.

However, you may have been relying on this wrapping in [resource routes][resource-routes] previously, so in v2 we will continue this automatic conversion. When Remix v2 detects a raw object returned from a resource route, it will log a deprecation warning and wrap the value in `json()` for easier opt-into the Single Fetch feature. At your convenience, you can add the `json()` call to your resource route handlers. Once you've addressed all of the deprecation warnings in your application's resource routes, you will be better prepared for the eventual Remix v3 upgrade.

```tsx filename=app/routes/resource.tsx bad
export function loader() {
return {
message: "My resource route",
};
}
```

```tsx filename=app/routes/resource.tsx good
import { json } from "@remix-run/react";

export function loader() {
return json({
message: "My resource route",
});
}
```

[future-flags]: ../file-conventions/remix-config#future
[should-revalidate]: ../route/should-revalidate
[entry-server]: ../file-conventions/entry.server
Expand All @@ -284,3 +308,4 @@ GET /a/b/c.data?_routes=routes/c
[starttransition]: https://react.dev/reference/react/startTransition
[headers]: ../route/headers
[mdn-headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers
[resource-routes]: ../guides/resource-routes
39 changes: 39 additions & 0 deletions integration/single-fetch-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,45 @@ test.describe("single-fetch", () => {
expect(await app.getHtml("#target")).toContain("Target");
});

test("wraps resource route naked object returns in json with a deprecation warning", async () => {
let oldConsoleWarn = console.warn;
let warnLogs: unknown[] = [];
console.warn = (...args) => warnLogs.push(...args);

let fixture = await createFixture(
{
config: {
future: {
unstable_singleFetch: true,
},
},
files: {
...files,
"app/routes/resource.tsx": js`
export function loader() {
return { message: "RESOURCE" };
}
`,
},
},
ServerMode.Development
);
let res = await fixture.requestResource("/resource");
expect(await res.json()).toEqual({
message: "RESOURCE",
});

expect(warnLogs).toEqual([
"⚠️ REMIX FUTURE CHANGE: Resource routes will no longer be able to return " +
"raw JavaScript objects in v3 when Single Fetch becomes the default. You " +
"can prepare for this change at your convenience by wrapping the data " +
"returned from your `loader` function in the `routes/resource` route with " +
"`json()`. For instructions on making this change see " +
"https://remix.run/docs/en/v2.9.2/guides/single-fetch#resource-routes",
]);
console.warn = oldConsoleWarn;
});

test.describe("client loaders", () => {
test("when no routes have client loaders", async ({ page }) => {
let fixture = await createFixture(
Expand Down
13 changes: 13 additions & 0 deletions packages/remix-server-runtime/deprecations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function resourceRouteJsonWarning(
type: "loader" | "action",
routeId: string
) {
return (
"⚠️ REMIX FUTURE CHANGE: Resource routes will no longer be able to " +
"return raw JavaScript objects in v3 when Single Fetch becomes the default. " +
"You can prepare for this change at your convenience by wrapping the data " +
`returned from your \`${type}\` function in the \`${routeId}\` route with ` +
"`json()`. For instructions on making this change see " +
"https://remix.run/docs/en/v2.9.2/guides/single-fetch#resource-routes"
);
}
16 changes: 16 additions & 0 deletions packages/remix-server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
isRedirectResponse,
isRedirectStatusCode,
isResponse,
json,
} from "./responses";
import { createServerHandoffString } from "./serverHandoff";
import { getDevServerHooks } from "./dev";
Expand All @@ -43,6 +44,7 @@ import {
singleFetchLoaders,
SingleFetchRedirectSymbol,
} from "./single-fetch";
import { resourceRouteJsonWarning } from "./deprecations";

export type RequestHandler = (
request: Request,
Expand Down Expand Up @@ -226,6 +228,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
) {
response = await handleResourceRequest(
serverMode,
_build,
staticHandler,
matches.slice(-1)[0].route.id,
request,
Expand Down Expand Up @@ -559,6 +562,7 @@ async function handleDocumentRequest(

async function handleResourceRequest(
serverMode: ServerMode,
build: ServerBuild,
staticHandler: StaticHandler,
routeId: string,
request: Request,
Expand All @@ -573,13 +577,25 @@ async function handleResourceRequest(
routeId,
requestContext: loadContext,
});

if (typeof response === "object") {
invariant(
!(DEFERRED_SYMBOL in response),
`You cannot return a \`defer()\` response from a Resource Route. Did you ` +
`forget to export a default UI component from the "${routeId}" route?`
);
}

if (build.future.unstable_singleFetch && !isResponse(response)) {
console.warn(
resourceRouteJsonWarning(
request.method === "GET" ? "loader" : "action",
routeId
)
);
response = json(response);
}

// callRouteLoader/callRouteAction always return responses (w/o single fetch).
// With single fetch, users should always be Responses from resource routes
invariant(
Expand Down

0 comments on commit 56e0a3f

Please sign in to comment.