Skip to content

Commit 677bfc6

Browse files
committed
useTransition, useActionData, useFetcher, shouldReload
This is kinda big but opens up a lot of use cases, - can build better pending navigation UI with useTransition telling app more detailed information (state, type) - replaces usePendingFormSubmit and usePendingLocation - actually aborts stale submissions/loads - fixes bugs around interrupted submissions/navigations - actions can return data now - super useful for form validation, no more screwing around with sessions - allows apps to call loaders and actions outside of navigation - manages cancellation of stale submissions and loads - reloads route data after actions - commits the freshest reloaded data along the way when there are multiple inflight allows route modules to decide if they should reload or not - after submissions - when the search params change - when the same href is navigated to other stuff - reloads route data when the same href is navigated to - does not create ghost history entries on interrupted navigation These old hooks still work, but have been deprecated for the new hooks. - useRouteData -> useLoaderData - usePendingFormSubmit -> useTransition().submission - usePendingLocation -> useTransition().location Also includes a helping of docs updates Closes #169, #151, #175, #128, #54, #208
1 parent cdc29c9 commit 677bfc6

File tree

11 files changed

+66
-68
lines changed

11 files changed

+66
-68
lines changed

packages/remix-dev/__tests__/readConfig-test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ describe("readConfig", () => {
7171
"parentId": "root",
7272
"path": "action-errors-self-boundary",
7373
},
74+
"routes/actions": Object {
75+
"caseSensitive": false,
76+
"file": "routes/actions.tsx",
77+
"id": "routes/actions",
78+
"parentId": "root",
79+
"path": "actions",
80+
},
7481
"routes/blog/hello-world": Object {
7582
"caseSensitive": false,
7683
"file": "routes/blog/hello-world.mdx",
@@ -106,6 +113,13 @@ describe("readConfig", () => {
106113
"parentId": "root",
107114
"path": "empty",
108115
},
116+
"routes/fetchers": Object {
117+
"caseSensitive": false,
118+
"file": "routes/fetchers.tsx",
119+
"id": "routes/fetchers",
120+
"parentId": "root",
121+
"path": "fetchers",
122+
},
109123
"routes/gists": Object {
110124
"caseSensitive": false,
111125
"file": "routes/gists.jsx",

packages/remix-dev/compiler.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,8 @@ const browserSafeRouteExports: { [name: string]: boolean } = {
431431
default: true,
432432
handle: true,
433433
links: true,
434-
meta: true
434+
meta: true,
435+
shouldReload: true
435436
};
436437

437438
/**

packages/remix-dev/compiler/assets.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ interface AssetsManifest {
2424
caseSensitive?: boolean;
2525
module: string;
2626
imports?: string[];
27-
hasAction?: boolean;
28-
hasLoader?: boolean;
27+
hasAction: boolean;
28+
hasLoader: boolean;
29+
hasErrorBoundary: boolean;
2930
};
3031
};
3132
}
@@ -90,7 +91,8 @@ export async function createAssetsManifest(
9091
module: resolveUrl(key),
9192
imports: resolveImports(output.imports),
9293
hasAction: sourceExports.includes("action"),
93-
hasLoader: sourceExports.includes("loader")
94+
hasLoader: sourceExports.includes("loader"),
95+
hasErrorBoundary: sourceExports.includes("ErrorBoundary")
9496
};
9597
}
9698
}

packages/remix-server-runtime/__tests__/data-test.ts

-31
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,6 @@
11
import { ServerBuild } from "../build";
22
import { createRequestHandler } from "../server";
33

4-
describe("actions", () => {
5-
it("returns a redirect when actions return a string", async () => {
6-
let location = "/just/a/string";
7-
let action = async () => location;
8-
9-
let routeId = "routes/random";
10-
let build = ({
11-
routes: {
12-
[routeId]: {
13-
id: routeId,
14-
path: "/random",
15-
module: { action }
16-
}
17-
}
18-
} as unknown) as ServerBuild;
19-
20-
let handler = createRequestHandler(build);
21-
22-
let request = new Request("http://example.com/random", {
23-
method: "POST",
24-
headers: {
25-
"Content-Type": "application/json"
26-
}
27-
});
28-
29-
let res = await handler(request);
30-
expect(res.status).toBe(303);
31-
expect(res.headers.get("location")).toBe(location);
32-
});
33-
});
34-
354
describe("loaders", () => {
365
// so that HTML/Fetch requests are the same, and so redirects don't hang on to
376
// this param for no reason

packages/remix-server-runtime/data.ts

+5-16
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function loadRouteData(
3232
if (result === undefined) {
3333
throw new Error(
3434
`You defined a loader for route "${routeId}" but didn't return ` +
35-
`anything from your \`loader\` function. We can't do everything for you! 😅`
35+
`anything from your \`loader\` function. Please return a value or \`null\`.`
3636
);
3737
}
3838

@@ -58,25 +58,14 @@ export async function callRouteAction(
5858

5959
let result = await routeModule.action({ request, context, params });
6060

61-
if (typeof result === "string") {
62-
return new Response("", {
63-
status: 303,
64-
headers: { Location: result }
65-
});
66-
}
67-
68-
if (!isResponse(result) || result.headers.get("Location") == null) {
61+
if (result === undefined) {
6962
throw new Error(
70-
`You made a ${request.method} request to ${request.url} but did not return ` +
71-
`a redirect. Please \`return newUrlString\` or \`return redirect(newUrl)\` from ` +
72-
`your \`action\` function to avoid reposts when users click the back button.`
63+
`You defined an action for route "${routeId}" but didn't return ` +
64+
`anything from your \`action\` function. Please return a value or \`null\`.`
7365
);
7466
}
7567

76-
return new Response("", {
77-
status: 303,
78-
headers: result.headers
79-
});
68+
return isResponse(result) ? result : json(result);
8069
}
8170

8271
function isResponse(value: any): value is Response {

packages/remix-server-runtime/entry.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface EntryContext {
1414
manifest: AssetsManifest;
1515
matches: RouteMatch<EntryRoute>[];
1616
routeData: RouteData;
17+
actionData?: RouteData;
1718
routeModules: RouteModules<EntryRouteModule>;
1819
serverHandoffString?: string;
1920
}

packages/remix-server-runtime/headers.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,26 @@ import type { RouteMatch } from "./routeMatching";
77
export function getDocumentHeaders(
88
build: ServerBuild,
99
matches: RouteMatch<ServerRoute>[],
10-
routeLoaderResponses: Response[]
10+
routeLoaderResponses: Response[],
11+
actionResponse?: Response
1112
): Headers {
1213
return matches.reduce((parentHeaders, match, index) => {
1314
let routeModule = build.routes[match.route.id].module;
1415
let loaderHeaders = routeLoaderResponses[index]
1516
? routeLoaderResponses[index].headers
1617
: new Headers();
18+
let actionHeaders = actionResponse ? actionResponse.headers : new Headers();
1719
let headers = new Headers(
1820
routeModule.headers
1921
? typeof routeModule.headers === "function"
20-
? routeModule.headers({ loaderHeaders, parentHeaders })
22+
? routeModule.headers({ loaderHeaders, parentHeaders, actionHeaders })
2123
: routeModule.headers
2224
: undefined
2325
);
2426

2527
// Automatically preserve Set-Cookie headers that were set either by the
2628
// loader or by a parent route.
29+
prependCookies(actionHeaders, headers);
2730
prependCookies(loaderHeaders, headers);
2831
prependCookies(parentHeaders, headers);
2932

packages/remix-server-runtime/routeData.ts

+4
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ export async function createRouteData(
1818
return memo;
1919
}, {} as RouteData);
2020
}
21+
22+
export async function createActionData(response: Response): Promise<RouteData> {
23+
return extractData(response);
24+
}

packages/remix-server-runtime/routeModules.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ export interface RouteModules<RouteModule> {
1515
*/
1616
export interface ActionFunction {
1717
(args: { request: Request; context: AppLoadContext; params: Params }):
18-
| Promise<Response | string>
19-
| Response
20-
| string;
18+
| Promise<Response>
19+
| Response;
2120
}
2221

2322
/**
@@ -30,9 +29,11 @@ export type ErrorBoundaryComponent = ComponentType<{ error: Error }>;
3029
* will be merged with (and take precedence over) headers from parent routes.
3130
*/
3231
export interface HeadersFunction {
33-
(args: { loaderHeaders: Headers; parentHeaders: Headers }):
34-
| Headers
35-
| HeadersInit;
32+
(args: {
33+
loaderHeaders: Headers;
34+
parentHeaders: Headers;
35+
actionHeaders: Headers;
36+
}): Headers | HeadersInit;
3637
}
3738

3839
/**

packages/remix-server-runtime/routes.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ interface Route {
1414
}
1515

1616
export interface EntryRoute extends Route {
17-
hasAction?: boolean;
18-
hasLoader?: boolean;
17+
hasAction: boolean;
18+
hasLoader: boolean;
19+
hasErrorBoundary: boolean;
1920
imports?: string[];
2021
module: string;
2122
}

packages/remix-server-runtime/server.ts

+20-7
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { matchServerRoutes } from "./routeMatching";
1313
import { ServerMode, isServerMode } from "./mode";
1414
import type { ServerRoute } from "./routes";
1515
import { createRoutes } from "./routes";
16-
import { createRouteData } from "./routeData";
16+
import { createActionData, createRouteData } from "./routeData";
1717
import { json } from "./responses";
1818
import { createServerHandoffString } from "./serverHandoff";
1919

@@ -157,18 +157,19 @@ async function handleDocumentRequest(
157157
};
158158

159159
let actionErrored: boolean = false;
160+
let actionResponse: Response | undefined;
160161

161162
if (isActionRequest(request)) {
162163
let leafMatch = matches[matches.length - 1];
163164
try {
164-
let actionResponse = await callRouteAction(
165+
actionResponse = await callRouteAction(
165166
build,
166167
leafMatch.route.id,
167-
request,
168+
request.clone(),
168169
loadContext,
169170
leafMatch.params
170171
);
171-
if (actionResponse && isRedirectResponse(actionResponse)) {
172+
if (isRedirectResponse(actionResponse)) {
172173
return actionResponse;
173174
}
174175
} catch (error) {
@@ -183,7 +184,10 @@ async function handleDocumentRequest(
183184

184185
let matchesToLoad = actionErrored
185186
? getMatchesUpToDeepestErrorBoundary(
186-
// get rid of the action, we know we don't want to call it's loader
187+
// get rid of the action, we don't want to call it's loader either
188+
// because we'll be rendering the error boundary, if you can get access
189+
// to the loader data in the error boundary then how the heck is it
190+
// supposed to deal with errors in the loader, too?
187191
matches.slice(0, -1)
188192
)
189193
: matches;
@@ -278,18 +282,27 @@ async function handleDocumentRequest(
278282
let headers = getDocumentHeaders(
279283
build,
280284
renderableMatches,
281-
routeLoaderResponses
285+
routeLoaderResponses,
286+
actionResponse
282287
);
283288
let entryMatches = createEntryMatches(renderableMatches, build.assets.routes);
284289
let routeData = await createRouteData(
285290
renderableMatches,
286291
routeLoaderResponses
287292
);
293+
let actionData = actionResponse
294+
? {
295+
[matches[matches.length - 1].route.id]: await createActionData(
296+
actionResponse
297+
)
298+
}
299+
: undefined;
288300
let routeModules = createEntryRouteModules(build.routes);
289301
let serverHandoff = {
290302
matches: entryMatches,
291303
componentDidCatchEmulator,
292-
routeData
304+
routeData,
305+
actionData
293306
};
294307
let entryContext: EntryContext = {
295308
...serverHandoff,

0 commit comments

Comments
 (0)