diff --git a/packages/toolkit/src/query/react/HydrateEndpoints.cc.tsx b/packages/toolkit/src/query/react/HydrateEndpoints.cc.tsx
new file mode 100644
index 0000000000..68ed2ebd39
--- /dev/null
+++ b/packages/toolkit/src/query/react/HydrateEndpoints.cc.tsx
@@ -0,0 +1,75 @@
+import { useStore } from 'react-redux'
+
+export interface EndpointRequest {
+  apiPath: string
+  serializedQueryArgs: string
+  resolvedAndTransformedData: Promise<unknown>
+}
+
+interface HydrateEndpointsProps {
+  immediateRequests: Array<EndpointRequest>
+  lateRequests: AsyncGenerator<EndpointRequest>
+  children?: any
+}
+
+const seen = new WeakSet<
+  Array<EndpointRequest> | AsyncGenerator<EndpointRequest>
+>()
+
+export function HydrateEndpoints({
+  immediateRequests,
+  lateRequests,
+  children,
+}: HydrateEndpointsProps) {
+  if (!seen.has(immediateRequests)) {
+    seen.add(immediateRequests)
+    for (const request of immediateRequests) {
+      handleRequest(request)
+    }
+  }
+  if (!seen.has(lateRequests)) {
+    seen.add(lateRequests)
+    handleLateRequests()
+    async function handleLateRequests() {
+      for await (const request of lateRequests) {
+        for (const request of immediateRequests) {
+          handleRequest(request)
+        }
+      }
+    }
+  }
+  const store = useStore()
+  return children
+
+  async function handleRequest(request: EndpointRequest) {
+    store.dispatch({
+      type: 'simulate-endpoint-start',
+      payload: {
+        serializedQueryArgs: request.serializedQueryArgs,
+        apiPath: request.apiPath,
+      },
+    })
+    try {
+      const data = await request.resolvedAndTransformedData
+      store.dispatch({
+        type: 'simulate-endpoint-success',
+        payload: {
+          data,
+          serializedQueryArgs: request.serializedQueryArgs,
+          apiPath: request.apiPath,
+        },
+      })
+    } catch (error) {
+      store.dispatch({
+        type: 'simulate-endpoint-error',
+        payload: {
+          serializedQueryArgs: request.serializedQueryArgs,
+          apiPath: request.apiPath,
+          // no error details here as it won't be transported over by React
+          // to not leak sensitive information from the server
+          // that's a good thing
+        },
+      })
+    }
+  }
+}
diff --git a/packages/toolkit/src/query/react/PrefetchEndpoints.tsx b/packages/toolkit/src/query/react/PrefetchEndpoints.tsx
new file mode 100644
index 0000000000..c0164f34a4
--- /dev/null
+++ b/packages/toolkit/src/query/react/PrefetchEndpoints.tsx
@@ -0,0 +1,103 @@
+import { createApi } from '.'
+import type { BaseQueryFn } from '../baseQueryTypes'
+import type { ApiEndpointQuery } from '../core'
+import type { QueryDefinition } from '../endpointDefinitions'
+import { fetchBaseQuery } from '../fetchBaseQuery'
+import type { EndpointRequest } from './HydrateEndpoints.cc'
+// this needs to be a separately bundled entry point prefixed with "use client"
+import { HydrateEndpoints } from './HydrateEndpoints.cc'
+
+interface PrefetchEndpointsProps<BaseQuery extends BaseQueryFn> {
+  baseQuery: BaseQueryFn
+  run: (
+    prefetchEndpoint: <QueryArg, ReturnType>(
+      endpoint: ApiEndpointQuery<
+        QueryDefinition<QueryArg, BaseQuery, any, ReturnType, any>,
+        any
+      >,
+      arg: QueryArg,
+    ) => Promise<ReturnType>,
+  ) => Promise<void> | undefined
+  children?: any
+}
+
+export function PrefetchEndpoints<BaseQuery extends BaseQueryFn>({
+  baseQuery,
+  run,
+  children,
+}: PrefetchEndpointsProps<BaseQuery>) {
+  const immediateRequests: Array<EndpointRequest> = []
+  const lateRequests = generateRequests()
+  async function* generateRequests(): AsyncGenerator<EndpointRequest> {
+    let resolveNext: undefined | PromiseWithResolvers<EndpointRequest>
+    const running = run((endpoint, arg) => {
+      // something something magic
+      const request = {
+        serializedQueryArgs: '...',
+        resolvedAndTransformedData: {}, // ...
+      } as any as EndpointRequest
+      if (!resolveNext) {
+        immediateRequests.push(request)
+      } else {
+        const oldResolveNext = resolveNext
+        resolveNext = Promise.withResolvers()
+        oldResolveNext.resolve(request)
+      }
+      return request.resolvedAndTransformedData
+    })
+
+    // not an async function, no need to wait for late requests
+    if (!running) return
+
+    let runningResolved = false
+    running.then(() => {
+      runningResolved = true
+    })
+
+    resolveNext = Promise.withResolvers()
+    while (!runningResolved) {
+      yield await resolveNext.promise
+    }
+  }
+  return (
+    <HydrateEndpoints
+      immediateRequests={immediateRequests}
+      lateRequests={lateRequests}
+    >
+      {children}
+    </HydrateEndpoints>
+  )
+}
+
+// usage:
+
+const baseQuery = fetchBaseQuery()
+const api = createApi({
+  baseQuery,
+  endpoints: (build) => ({
+    foo: build.query<string, string>({
+      query(arg) {
+        return { url: '/foo' + arg }
+      },
+    }),
+  }),
+})
+
+function Page() {
+  return (
+    <PrefetchEndpoints
+      baseQuery={baseQuery}
+      run={async (prefetch) => {
+        // immediate prefetching
+        const promise1 = prefetch(api.endpoints.foo, 'bar')
+        const promise2 = prefetch(api.endpoints.foo, 'baz')
+        // and a "dependent endpoint" that can only be prefetched with the result of the first two
+        const result1 = await promise1
+        const result2 = await promise2
+        prefetch(api.endpoints.foo, result1 + result2)
+      }}
+    >
+      foo
+    </PrefetchEndpoints>
+  )
+}
diff --git a/packages/toolkit/src/query/react/cc-entry-point.ts b/packages/toolkit/src/query/react/cc-entry-point.ts
new file mode 100644
index 0000000000..8ab2d1c778
--- /dev/null
+++ b/packages/toolkit/src/query/react/cc-entry-point.ts
@@ -0,0 +1,3 @@
+'use client'
+
+export { HydrateEndpoints } from './HydrateEndpoints.cc.jsx'