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

feat: add custom response decoding option #11086

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/decode-response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"react-router-dom": minor
"react-router": minor
"@remix-run/router": minor
---

add decodeResponse option to createStaticHandler, createStaticRouter, createBrowserRouter, and createHashRouter
4 changes: 4 additions & 0 deletions docs/routers/create-browser-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ createBrowserRouter(routes, {
<Link to="/" />; // results in <a href="/app/" />
```

## `decodeResponse`

An optional hook to implement custom response decoding logic. This is where you could hook up libraries such as `super-json` or `turbo-stream`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a Type Declaration section a bit higher up that we should add this too also


## `future`

An optional set of [Future Flags][api-development-strategy] to enable for this Router. We recommend opting into newly released future flags sooner rather than later to ease your eventual migration to v7.
Expand Down
4 changes: 4 additions & 0 deletions docs/routers/create-static-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ interface StaticHandler {

These are the same `routes`/`basename` you would pass to [`createBrowserRouter`][createbrowserrouter]

## `decodeResponse`

This is the same you would pass to [`createBrowserRouter`][createbrowserrouter]

## `handler.query(request, opts)`

The `handler.query()` method takes in a Fetch request, performs route matching, and executes all relevant route action/loader methods depending on the request. The return `context` value contains all of the information required to render the HTML document for the request (route-level `actionData`, `loaderData`, `errors`, etc.). If any of the matched routes return or throw a redirect response, then `query()` will return that redirect in the form of Fetch `Response`.
Expand Down
103 changes: 103 additions & 0 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,109 @@ function testDomRouter(
`);
});

it("executes route loaders on navigation with decodeRoute", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("executes route loaders on navigation with decodeRoute", async () => {
it("executes route loaders on navigation with decodeResponse", async () => {

let barDefer = createDeferred();

let router = createTestRouter(
createRoutesFromElements(
<Route path="/" element={<Layout />}>
<Route path="foo" element={<Foo />} />
<Route
path="bar"
loader={() => barDefer.promise}
element={<Bar />}
/>
</Route>
),
{
window: getWindow("/foo"),
async decodeResponse(response, defaultDecode) {
let contentType = response.headers.get("Content-Type");
if (contentType === "text/custom") {
const text = await response.text();
switch (text) {
case "bar":
return { message: "Bar Loader" };
default:
return text;
}
}

return defaultDecode();
},
}
);
let { container } = render(<RouterProvider router={router} />);

function Layout() {
let navigation = useNavigation();
return (
<div>
<Link to="/bar">Link to Bar</Link>
<div id="output">
<p>{navigation.state}</p>
<Outlet />
</div>
</div>
);
}

function Foo() {
return <h1>Foo</h1>;
}
function Bar() {
let data = useLoaderData() as { message: string };
return <h1>{data.message}</h1>;
}

expect(getHtml(container.querySelector("#output")!))
.toMatchInlineSnapshot(`
"<div
id="output"
>
<p>
idle
</p>
<h1>
Foo
</h1>
</div>"
`);

fireEvent.click(screen.getByText("Link to Bar"));
expect(getHtml(container.querySelector("#output")!))
.toMatchInlineSnapshot(`
"<div
id="output"
>
<p>
loading
</p>
<h1>
Foo
</h1>
</div>"
`);

barDefer.resolve(
new Response("bar", { headers: { "Content-Type": "text/custom" } })
);
await waitFor(() => screen.getByText("idle"));
expect(getHtml(container.querySelector("#output")!))
.toMatchInlineSnapshot(`
"<div
id="output"
>
<p>
idle
</p>
<h1>
Bar Loader
</h1>
</div>"
`);
});

it("executes lazy route loaders on navigation", async () => {
let barDefer = createDeferred();

Expand Down
4 changes: 4 additions & 0 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from "react-router";
import type {
BrowserHistory,
DecodeResponseFunction,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be re-exported from react-router-dom?

Fetcher,
FormEncType,
FormMethod,
Expand Down Expand Up @@ -231,6 +232,7 @@ interface DOMRouterOpts {
future?: Partial<Omit<RouterFutureConfig, "v7_prependBasename">>;
hydrationData?: HydrationState;
window?: Window;
decodeResponse?: DecodeResponseFunction;
}

export function createBrowserRouter(
Expand All @@ -248,6 +250,7 @@ export function createBrowserRouter(
routes,
mapRouteProperties,
window: opts?.window,
decodeResponse: opts?.decodeResponse,
}).initialize();
}

Expand All @@ -266,6 +269,7 @@ export function createHashRouter(
routes,
mapRouteProperties,
window: opts?.window,
decodeResponse: opts?.decodeResponse,
}).initialize();
}

Expand Down
108 changes: 108 additions & 0 deletions packages/react-router/__tests__/data-memory-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,114 @@ describe("createMemoryRouter", () => {
`);
});

it("executes route loaders on navigation with decodeResponse", async () => {
let barDefer = createDeferred();
let router = createMemoryRouter(
createRoutesFromElements(
<Route path="/" element={<Layout />}>
<Route path="foo" element={<Foo />} />
<Route path="bar" loader={() => barDefer.promise} element={<Bar />} />
</Route>
),
{
initialEntries: ["/foo"],
async decodeResponse(response, defaultDecode) {
let contentType = response.headers.get("Content-Type");
if (contentType === "text/custom") {
const text = await response.text();
switch (text) {
case "bar":
return { message: "Bar Loader" };
default:
return text;
}
}

return defaultDecode();
},
}
);
let { container } = render(<RouterProvider router={router} />);

function Layout() {
let navigation = useNavigation();
return (
<div>
<MemoryNavigate to="/bar">Link to Bar</MemoryNavigate>
<p>{navigation.state}</p>
<Outlet />
</div>
);
}

function Foo() {
return <h1>Foo</h1>;
}
function Bar() {
let data = useLoaderData() as { message: string };
return <h1>{data?.message}</h1>;
}

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<div>
<a
href="/bar"
>
Link to Bar
</a>
<p>
idle
</p>
<h1>
Foo
</h1>
</div>
</div>"
`);

fireEvent.click(screen.getByText("Link to Bar"));
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<div>
<a
href="/bar"
>
Link to Bar
</a>
<p>
loading
</p>
<h1>
Foo
</h1>
</div>
</div>"
`);

barDefer.resolve(
new Response("bar", { headers: { "Content-Type": "text/custom" } })
);
await waitFor(() => screen.getByText("idle"));
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<div>
<a
href="/bar"
>
Link to Bar
</a>
<p>
idle
</p>
<h1>
Bar Loader
</h1>
</div>
</div>"
`);
});

it("executes route actions/loaders on submission navigations", async () => {
let barDefer = createDeferred();
let barActionDefer = createDeferred();
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ActionFunctionArgs,
Blocker,
BlockerFunction,
DecodeResponseFunction,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be re-exported?

ErrorResponse,
Fetcher,
HydrationState,
Expand Down Expand Up @@ -272,6 +273,7 @@ export function createMemoryRouter(
hydrationData?: HydrationState;
initialEntries?: InitialEntry[];
initialIndex?: number;
decodeResponse?: DecodeResponseFunction;
}
): RemixRouter {
return createRouter({
Expand All @@ -287,6 +289,7 @@ export function createMemoryRouter(
hydrationData: opts?.hydrationData,
routes,
mapRouteProperties,
decodeResponse: opts?.decodeResponse,
}).initialize();
}

Expand Down