diff --git a/packages/remix-cloudflare/index.ts b/packages/remix-cloudflare/index.ts
index a21c55b7516..d1463dbcaf7 100644
--- a/packages/remix-cloudflare/index.ts
+++ b/packages/remix-cloudflare/index.ts
@@ -13,7 +13,9 @@ export {
   createRequestHandler,
   createSession,
   unstable_defineLoader,
+  unstable_defineClientLoader,
   unstable_defineAction,
+  unstable_defineClientAction,
   defer,
   broadcastDevReady,
   logDevReady,
diff --git a/packages/remix-deno/index.ts b/packages/remix-deno/index.ts
index bd91abef915..160c21d9d50 100644
--- a/packages/remix-deno/index.ts
+++ b/packages/remix-deno/index.ts
@@ -17,7 +17,9 @@ export {
   broadcastDevReady,
   createSession,
   unstable_defineLoader,
+  unstable_defineClientLoader,
   unstable_defineAction,
+  unstable_defineClientAction,
   defer,
   isCookie,
   isSession,
diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts
index eaf64599f3c..e1587d6dcbc 100644
--- a/packages/remix-node/index.ts
+++ b/packages/remix-node/index.ts
@@ -25,7 +25,9 @@ export {
   createRequestHandler,
   createSession,
   unstable_defineLoader,
+  unstable_defineClientLoader,
   unstable_defineAction,
+  unstable_defineClientAction,
   defer,
   broadcastDevReady,
   logDevReady,
diff --git a/packages/remix-react/future/single-fetch.d.ts b/packages/remix-react/future/single-fetch.d.ts
index d5be4e5e110..fa37a8a338b 100644
--- a/packages/remix-react/future/single-fetch.d.ts
+++ b/packages/remix-react/future/single-fetch.d.ts
@@ -1,26 +1,14 @@
 import type { MetaArgs, UIMatch, UNSAFE_MetaMatch } from "@remix-run/react";
 import type {
-  Loader,
-  Action,
-  SerializeFrom,
-  TypedDeferredData,
-  TypedResponse,
+  unstable_Loader as Loader,
+  unstable_Action as Action,
+  unstable_Serialize as Serialize,
 } from "@remix-run/server-runtime";
 import type {
   useFetcher as useFetcherRR,
   FetcherWithComponents,
 } from "react-router-dom";
 
-// Backwards-compatible type for Remix v2 where json/defer still use the old types,
-// and only non-json/defer returns use the new types.  This allows for incremental
-// migration of loaders to return naked objects.  In the next major version,
-// json/defer will be removed so everything will use the new simplified typings.
-// prettier-ignore
-type Serialize<T extends Loader | Action> =
-  Awaited<ReturnType<T>> extends TypedDeferredData<infer D> ? D :
-  Awaited<ReturnType<T>> extends TypedResponse<Record<string, unknown>> ? SerializeFrom<T> :
-  Awaited<ReturnType<T>>;
-
 declare module "@remix-run/react" {
   export function useLoaderData<T extends Loader>(): Serialize<T>;
 
diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts
index ac614c95313..2840bdc6a33 100644
--- a/packages/remix-server-runtime/index.ts
+++ b/packages/remix-server-runtime/index.ts
@@ -5,13 +5,22 @@ export {
   parseMultipartFormData as unstable_parseMultipartFormData,
 } from "./formData";
 export { defer, json, redirect, redirectDocument } from "./responses";
+
+export {
+  SingleFetchRedirectSymbol as UNSAFE_SingleFetchRedirectSymbol,
+  defineLoader as unstable_defineLoader,
+  defineClientLoader as unstable_defineClientLoader,
+  defineAction as unstable_defineAction,
+  defineClientAction as unstable_defineClientAction,
+} from "./single-fetch";
 export type {
-  Loader,
-  Action,
+  Loader as unstable_Loader,
+  Action as unstable_Action,
+  Serialize as unstable_Serialize,
   SingleFetchResult as UNSAFE_SingleFetchResult,
   SingleFetchResults as UNSAFE_SingleFetchResults,
 } from "./single-fetch";
-export { SingleFetchRedirectSymbol as UNSAFE_SingleFetchRedirectSymbol } from "./single-fetch";
+
 export { createRequestHandler } from "./server";
 export {
   createSession,
@@ -87,7 +96,3 @@ export type {
   UploadHandler,
   UploadHandlerPart,
 } from "./reexport";
-export {
-  defineLoader as unstable_defineLoader,
-  defineAction as unstable_defineAction,
-} from "./reexport";
diff --git a/packages/remix-server-runtime/reexport.ts b/packages/remix-server-runtime/reexport.ts
index 2a0316600b5..cc7fc46f507 100644
--- a/packages/remix-server-runtime/reexport.ts
+++ b/packages/remix-server-runtime/reexport.ts
@@ -61,5 +61,3 @@ export type {
   SessionStorage,
   FlashSessionData,
 } from "./sessions";
-
-export { defineLoader, defineAction } from "./single-fetch";
diff --git a/packages/remix-server-runtime/single-fetch.ts b/packages/remix-server-runtime/single-fetch.ts
index 3104eb62b4f..1ba3e2bb771 100644
--- a/packages/remix-server-runtime/single-fetch.ts
+++ b/packages/remix-server-runtime/single-fetch.ts
@@ -23,6 +23,7 @@ import type {
 import { ResponseStubOperationsSymbol } from "./routeModules";
 import type { TypedDeferredData, TypedResponse } from "./responses";
 import { isDeferredData, isRedirectStatusCode, isResponse } from "./responses";
+import type { SerializeFrom } from "./serialize";
 
 export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect");
 const ResponseStubActionSymbol = Symbol("ResponseStubAction");
@@ -533,25 +534,50 @@ type DataFunctionReturnValue =
   | TypedDeferredData<Record<string, unknown>>
   | TypedResponse<Record<string, unknown>>;
 
+// Backwards-compatible type for Remix v2 where json/defer still use the old types,
+// and only non-json/defer returns use the new types.  This allows for incremental
+// migration of loaders to return naked objects.  In the next major version,
+// json/defer will be removed so everything will use the new simplified typings.
+// prettier-ignore
+export type Serialize<T extends Loader | ClientLoader | Action | ClientAction> =
+  Awaited<ReturnType<T>> extends TypedDeferredData<infer D> ? D :
+  Awaited<ReturnType<T>> extends TypedResponse<Record<string, unknown>> ? SerializeFrom<T> :
+  Awaited<ReturnType<T>>;
+
+// loader
 type LoaderArgs = RRLoaderArgs<AppLoadContext> & {
   // Context is always provided in Remix, and typed for module augmentation support.
   context: AppLoadContext;
   response: ResponseStub;
 };
-
 export type Loader = (
   args: LoaderArgs
 ) => MaybePromise<DataFunctionReturnValue>;
+export let defineLoader = <T extends Loader>(loader: T): T => loader;
 
+// clientLoader
+type ClientLoaderArgs = RRLoaderArgs<undefined> & {
+  serverLoader: <T extends Loader>() => Promise<Serialize<T>>;
+};
+type ClientLoader = (args: ClientLoaderArgs) => MaybePromise<Serializable>;
+export let defineClientLoader = <T extends ClientLoader>(clientLoader: T): T =>
+  clientLoader;
+
+// action
 type ActionArgs = RRActionArgs<AppLoadContext> & {
   // Context is always provided in Remix, and typed for module augmentation support.
   context: AppLoadContext;
   response: ResponseStub;
 };
-
 export type Action = (
   args: ActionArgs
 ) => MaybePromise<DataFunctionReturnValue>;
-
-export let defineLoader = <T extends Loader>(loader: T): T => loader;
 export let defineAction = <T extends Action>(action: T): T => action;
+
+// clientAction
+type ClientActionArgs = RRActionArgs<undefined> & {
+  serverAction: <T extends Action>() => Promise<Serialize<T>>;
+};
+type ClientAction = (args: ClientActionArgs) => MaybePromise<Serializable>;
+export let defineClientAction = <T extends ClientAction>(clientAction: T): T =>
+  clientAction;