Skip to content

Commit 2c6c99e

Browse files
committed
Allow fetchers in non-DOM scenarios
1 parent b080bf7 commit 2c6c99e

File tree

3 files changed

+112
-24
lines changed

3 files changed

+112
-24
lines changed

packages/react-router/__tests__/data-router-no-dom-test.tsx

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as React from "react";
66
import renderer from "react-test-renderer";
7-
import { RouterProvider } from "../lib/dom/lib";
7+
import { RouterProvider, useFetcher } from "../lib/dom/lib";
88
import { createMemoryRouter } from "../lib/components";
99
import { useLoaderData, useNavigate } from "../lib/hooks";
1010

@@ -179,4 +179,112 @@ describe("RouterProvider works when no DOM APIs are available", () => {
179179

180180
unsubscribe();
181181
});
182+
183+
it("supports fetcher loads", async () => {
184+
let router = createMemoryRouter([
185+
{
186+
path: "/",
187+
Component: () => {
188+
let fetcher = useFetcher();
189+
return (
190+
<button onClick={() => fetcher.load("/fetch")}>
191+
Load fetcher
192+
{fetcher.data || ""}
193+
</button>
194+
);
195+
},
196+
},
197+
{
198+
path: "/fetch",
199+
loader() {
200+
return "LOADER";
201+
},
202+
},
203+
]);
204+
const component = renderer.create(<RouterProvider router={router} />);
205+
let tree = component.toJSON();
206+
expect(tree).toMatchInlineSnapshot(`
207+
<button
208+
onClick={[Function]}
209+
>
210+
Load fetcher
211+
</button>
212+
`);
213+
214+
await renderer.act(async () => {
215+
// @ts-expect-error
216+
tree.props.onClick();
217+
await new Promise((resolve) => setTimeout(resolve, 100));
218+
});
219+
220+
tree = component.toJSON();
221+
expect(tree).toMatchInlineSnapshot(`
222+
<button
223+
onClick={[Function]}
224+
>
225+
Load fetcher
226+
LOADER
227+
</button>
228+
`);
229+
});
230+
231+
it("supports fetcher submissions", async () => {
232+
let router = createMemoryRouter([
233+
{
234+
path: "/",
235+
Component: () => {
236+
let fetcher = useFetcher();
237+
return (
238+
<button
239+
onClick={() =>
240+
fetcher.submit(
241+
{ message: "echo" },
242+
{
243+
method: "post",
244+
action: "/fetch",
245+
encType: "application/json",
246+
}
247+
)
248+
}
249+
>
250+
Submit fetcher
251+
{fetcher.data?.message || ""}
252+
</button>
253+
);
254+
},
255+
},
256+
{
257+
path: "/fetch",
258+
async action({ request }) {
259+
let data = await request.json();
260+
return { message: data.message.toUpperCase() };
261+
},
262+
},
263+
]);
264+
const component = renderer.create(<RouterProvider router={router} />);
265+
let tree = component.toJSON();
266+
expect(tree).toMatchInlineSnapshot(`
267+
<button
268+
onClick={[Function]}
269+
>
270+
Submit fetcher
271+
</button>
272+
`);
273+
274+
await renderer.act(async () => {
275+
// @ts-expect-error
276+
tree.props.onClick();
277+
await new Promise((resolve) => setTimeout(resolve, 100));
278+
});
279+
280+
tree = component.toJSON();
281+
expect(tree).toMatchInlineSnapshot(`
282+
<button
283+
onClick={[Function]}
284+
>
285+
Submit fetcher
286+
ECHO
287+
</button>
288+
`);
289+
});
182290
});

packages/react-router/lib/dom/lib.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,7 +1173,7 @@ export interface NavLinkProps
11731173

11741174
/**
11751175
Wraps {@link Link | `<Link>`} with additional props for styling active and pending states.
1176-
1176+
11771177
- Automatically applies classes to the link based on its active and pending states, see {@link NavLinkProps.className}.
11781178
- Automatically applies `aria-current="page"` to the link when the link is active. See [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current) on MDN.
11791179
@@ -1910,15 +1910,6 @@ export interface FetcherSubmitFunction {
19101910
): Promise<void>;
19111911
}
19121912

1913-
function validateClientSideSubmission() {
1914-
if (typeof document === "undefined") {
1915-
throw new Error(
1916-
"You are calling submit during the server render. " +
1917-
"Try calling submit within a `useEffect` or callback instead."
1918-
);
1919-
}
1920-
}
1921-
19221913
let fetcherId = 0;
19231914
let getUniqueFetcherId = () => `__${String(++fetcherId)}__`;
19241915

@@ -1949,8 +1940,6 @@ export function useSubmit(): SubmitFunction {
19491940

19501941
return React.useCallback<SubmitFunction>(
19511942
async (target, options = {}) => {
1952-
validateClientSideSubmission();
1953-
19541943
let { action, method, encType, formData, body } = getFormSubmissionInfo(
19551944
target,
19561945
basename
@@ -2132,7 +2121,7 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
21322121

21332122
/**
21342123
Loads data from a route. Useful for loading data imperatively inside of user events outside of a normal button or form, like a combobox or search input.
2135-
2124+
21362125
```tsx
21372126
let fetcher = useFetcher()
21382127
@@ -2159,7 +2148,7 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
21592148

21602149
/**
21612150
Useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
2162-
2151+
21632152
Fetchers track their own, independent state and can be used to load data, submit forms, and generally interact with loaders and actions.
21642153
21652154
```tsx

packages/react-router/lib/router/router.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,6 @@ export function createRouter(init: RouterInit): Router {
789789
typeof routerWindow !== "undefined" &&
790790
typeof routerWindow.document !== "undefined" &&
791791
typeof routerWindow.document.createElement !== "undefined";
792-
const isServer = !isBrowser;
793792

794793
invariant(
795794
init.routes.length > 0,
@@ -1973,14 +1972,6 @@ export function createRouter(init: RouterInit): Router {
19731972
href: string | null,
19741973
opts?: RouterFetchOptions
19751974
) {
1976-
if (isServer) {
1977-
throw new Error(
1978-
"router.fetch() was called during the server render, but it shouldn't be. " +
1979-
"You are likely calling a useFetcher() method in the body of your component. " +
1980-
"Try moving it to a useEffect or a callback."
1981-
);
1982-
}
1983-
19841975
if (fetchControllers.has(key)) abortFetcher(key);
19851976
let flushSync = (opts && opts.unstable_flushSync) === true;
19861977

0 commit comments

Comments
 (0)