Skip to content

Commit

Permalink
Add clientAction support
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Nov 29, 2023
1 parent 1cc1bae commit 93f0623
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 15 deletions.
381 changes: 368 additions & 13 deletions integration/client-data-test.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/remix-dev/compiler/js/plugins/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Route = RemixConfig["routes"][string];
// If you change this, make sure you update loadRouteModuleWithBlockingLinks in
// remix-react/routes.ts
const browserSafeRouteExports: { [name: string]: boolean } = {
clientAction: true,
clientLoader: true,
ErrorBoundary: true,
HydrateFallback: true,
Expand Down
1 change: 1 addition & 0 deletions packages/remix-dev/compiler/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export async function create({
imports: resolveImports(output.imports),
hasAction: sourceExports.includes("action"),
hasLoader: sourceExports.includes("loader"),
hasClientAction: sourceExports.includes("clientAction"),
hasClientLoader: sourceExports.includes("clientLoader"),
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
};
Expand Down
1 change: 1 addition & 0 deletions packages/remix-dev/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type Manifest = {
imports?: string[];
hasAction: boolean;
hasLoader: boolean;
hasClientAction: boolean;
hasClientLoader: boolean;
hasErrorBoundary: boolean;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
caseSensitive: route.caseSensitive,
hasAction: sourceExports.includes("action"),
hasLoader: sourceExports.includes("loader"),
hasClientAction: sourceExports.includes("clientAction"),
hasClientLoader: sourceExports.includes("clientLoader"),
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
...resolveBuildAssetPaths(pluginConfig, viteManifest, routeFilePath),
Expand Down Expand Up @@ -431,6 +432,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
}`,
hasAction: sourceExports.includes("action"),
hasLoader: sourceExports.includes("loader"),
hasClientAction: sourceExports.includes("clientAction"),
hasClientLoader: sourceExports.includes("clientLoader"),
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
imports: [],
Expand Down
2 changes: 2 additions & 0 deletions packages/remix-react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export {

export type { HtmlLinkDescriptor } from "./links";
export type {
ClientActionFunction,
ClientActionFunctionArgs,
ClientLoaderFunction,
ClientLoaderFunctionArgs,
MetaArgs,
Expand Down
17 changes: 17 additions & 0 deletions packages/remix-react/routeModules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ComponentType } from "react";
import type {
ActionFunction as RRActionFunction,
ActionFunctionArgs as RRActionFunctionArgs,
LoaderFunction as RRLoaderFunction,
LoaderFunctionArgs as RRLoaderFunctionArgs,
DataRouteMatch,
Expand All @@ -18,6 +20,7 @@ export interface RouteModules {
}

export interface RouteModule {
clientAction?: ClientActionFunction;
clientLoader?: ClientLoaderFunction;
ErrorBoundary?: ErrorBoundaryComponent;
HydrateFallback?: HydrateFallbackComponent;
Expand All @@ -28,6 +31,20 @@ export interface RouteModule {
shouldRevalidate?: ShouldRevalidateFunction;
}

/**
* A function that handles data mutations for a route on the client
*/
export type ClientActionFunction = (
args: ClientActionFunctionArgs
) => ReturnType<RRActionFunction>;

/**
* Arguments passed to a route `clientAction` function
*/
export type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & {
serverAction: <T = AppData>() => Promise<SerializeFrom<T>>;
};

/**
* A function that loads data for a route on the client
*/
Expand Down
40 changes: 38 additions & 2 deletions packages/remix-react/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface Route {
export interface EntryRoute extends Route {
hasAction: boolean;
hasLoader: boolean;
hasClientAction: boolean;
hasClientLoader: boolean;
hasErrorBoundary: boolean;
imports?: string[];
Expand Down Expand Up @@ -189,8 +190,6 @@ export function createClientRoutes(
id: route.id,
index: route.index,
path: route.path,
action: ({ request }: ActionFunctionArgs) =>
prefetchStylesAndCallHandler(() => fetchServerAction(request)),
};

if (routeModule) {
Expand Down Expand Up @@ -262,6 +261,24 @@ export function createClientRoutes(
});
});
};

dataRoute.action = ({ request, params }: ActionFunctionArgs) => {
return prefetchStylesAndCallHandler(async () => {
if (!routeModule.clientAction) {
return fetchServerAction(request);
}

return routeModule.clientAction({
request,
params,
async serverAction() {
let result = await fetchServerAction(request);
let unwrapped = await unwrapServerResponse(result);
return unwrapped;
},
});
});
};
} else {
// If the lazy route does not have a client loader/action we want to call
// the server loader/action in parallel with the module load so we add
Expand All @@ -270,6 +287,10 @@ export function createClientRoutes(
dataRoute.loader = ({ request }: LoaderFunctionArgs) =>
prefetchStylesAndCallHandler(() => fetchServerLoader(request));
}
if (!route.hasClientAction) {
dataRoute.action = ({ request }: ActionFunctionArgs) =>
prefetchStylesAndCallHandler(() => fetchServerAction(request));
}

// Load all other modules via route.lazy()
dataRoute.lazy = async () => {
Expand All @@ -292,6 +313,19 @@ export function createClientRoutes(
});
}

if (mod.clientAction) {
let clientAction = mod.clientAction;
lazyRoute.action = (args) =>
clientAction({
...args,
async serverAction() {
let response = await fetchServerAction(args.request);
let result = await unwrapServerResponse(response);
return result;
},
});
}

if (needsRevalidation) {
lazyRoute.shouldRevalidate = wrapShouldRevalidateForHdr(
route.id,
Expand All @@ -302,6 +336,7 @@ export function createClientRoutes(

return {
...(lazyRoute.loader ? { loader: lazyRoute.loader } : {}),
...(lazyRoute.action ? { action: lazyRoute.action } : {}),
hasErrorBoundary: lazyRoute.hasErrorBoundary,
shouldRevalidate: lazyRoute.shouldRevalidate,
handle: lazyRoute.handle,
Expand Down Expand Up @@ -357,6 +392,7 @@ async function loadRouteModuleWithBlockingLinks(
return {
Component: getRouteModuleComponent(routeModule),
ErrorBoundary: routeModule.ErrorBoundary,
clientAction: routeModule.clientAction,
clientLoader: routeModule.clientLoader,
handle: routeModule.handle,
links: routeModule.links,
Expand Down
17 changes: 17 additions & 0 deletions packages/remix-server-runtime/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ export type ActionFunctionArgs = RRActionFunctionArgs<AppLoadContext> & {
context: AppLoadContext;
};

/**
* A function that handles data mutations for a route on the client
* @private Public API is exported from @remix-run/react
*/
type ClientActionFunction = (
args: ClientActionFunctionArgs
) => ReturnType<RRActionFunction>;

/**
* Arguments passed to a route `clientAction` function
* @private Public API is exported from @remix-run/react
*/
type ClientActionFunctionArgs = RRActionFunctionArgs<undefined> & {
serverAction: <T = AppData>() => Promise<SerializeFrom<T>>;
};

/**
* A function that loads data for a route on the server
*/
Expand Down Expand Up @@ -233,6 +249,7 @@ type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;
export type RouteHandle = unknown;

export interface EntryRouteModule {
clientAction?: ClientActionFunction;
clientLoader?: ClientLoaderFunction;
ErrorBoundary?: any; // Weakly typed because server-runtime is not React-aware
HydrateFallback?: any; // Weakly typed because server-runtime is not React-aware
Expand Down
1 change: 1 addition & 0 deletions packages/remix-testing/create-remix-stub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ function processRoutes(
// When testing routes, you should just be stubbing loader/action, not
// trying to re-implement the full loader/clientLoader/SSR/hydration flow.
// That is better tested via E2E tests.
hasClientAction: false,
hasClientLoader: false,
hasErrorBoundary: route.ErrorBoundary != null,
module: "build/stub-path-to-module.js", // any need for this?
Expand Down

0 comments on commit 93f0623

Please sign in to comment.