Skip to content

Commit 4dc10c1

Browse files
pass AsyncPropsManager to the component
1 parent 12afe85 commit 4dc10c1

File tree

8 files changed

+132
-0
lines changed

8 files changed

+132
-0
lines changed

packages/react-on-rails-pro/src/AsyncPropsManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ class AsyncPropsManager {
4646
}
4747

4848
endStream() {
49+
if (this.isClosed) {
50+
return;
51+
}
52+
4953
this.isClosed = true;
5054
this.propNameToPromiseController.forEach((promiseController, propName) => {
5155
if (!promiseController.resolved) {

packages/react-on-rails-pro/src/ReactOnRailsRSC.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { convertToError } from 'react-on-rails/serverRenderUtils';
2626
import handleError from './handleErrorRSC.ts';
2727
import ReactOnRails from './ReactOnRails.full.ts';
28+
import AsyncPropsManager from './AsyncPropsManager.ts';
2829

2930
import {
3031
streamServerRenderedComponent,
@@ -104,6 +105,26 @@ ReactOnRails.serverRenderRSCReactComponent = (options: RSCRenderParams) => {
104105
}
105106
};
106107

108+
function addAsyncPropsCapabilityToComponentProps<
109+
AsyncPropsType extends Record<string, unknown>,
110+
PropsType extends Record<string, unknown>,
111+
>(props: PropsType) {
112+
const asyncPropManager = new AsyncPropsManager();
113+
const propsAfterAddingAsyncProps = {
114+
...props,
115+
getReactOnRailsAsyncProp: <PropName extends keyof AsyncPropsType>(propName: PropName) => {
116+
return asyncPropManager.getProp(propName as string) as Promise<AsyncPropsType[PropName]>;
117+
},
118+
};
119+
120+
return {
121+
asyncPropManager,
122+
props: propsAfterAddingAsyncProps,
123+
};
124+
}
125+
126+
ReactOnRails.addAsyncPropsCapabilityToComponentProps = addAsyncPropsCapabilityToComponentProps;
127+
107128
ReactOnRails.isRSCBundle = true;
108129

109130
export * from 'react-on-rails/types';

packages/react-on-rails-pro/src/createReactOnRailsPro.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type ReactOnRailsProSpecificFunctions = Pick<
4545
| 'reactOnRailsStoreLoaded'
4646
| 'streamServerRenderedReactComponent'
4747
| 'serverRenderRSCReactComponent'
48+
| 'addAsyncPropsCapabilityToComponentProps'
4849
>;
4950

5051
// Pro client startup with immediate hydration support
@@ -133,6 +134,10 @@ export default function createReactOnRailsPro(
133134
serverRenderRSCReactComponent(): any {
134135
throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only');
135136
},
137+
138+
addAsyncPropsCapabilityToComponentProps() {
139+
throw new Error('addAsyncPropsCapabilityToComponentProps is supported in RSC bundle only');
140+
},
136141
};
137142

138143
// Type assertion is safe here because:
@@ -153,6 +158,11 @@ export default function createReactOnRailsPro(
153158
reactOnRailsPro.serverRenderRSCReactComponent;
154159
}
155160

161+
if (reactOnRailsPro.addAsyncPropsCapabilityToComponentProps) {
162+
reactOnRailsProSpecificFunctions.addAsyncPropsCapabilityToComponentProps =
163+
reactOnRailsPro.addAsyncPropsCapabilityToComponentProps;
164+
}
165+
156166
// Assign Pro-specific functions to the ReactOnRailsPro object using Object.assign
157167
// This pattern ensures we add exactly what's defined in the type, nothing more, nothing less
158168
Object.assign(reactOnRailsPro, reactOnRailsProSpecificFunctions);

packages/react-on-rails/src/base/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type BaseClientObjectType = Omit<
5151
| 'reactOnRailsStoreLoaded'
5252
| 'streamServerRenderedReactComponent'
5353
| 'serverRenderRSCReactComponent'
54+
| 'addAsyncPropsCapabilityToComponentProps'
5455
>;
5556

5657
// Cache to track created objects and their registries

packages/react-on-rails/src/createReactOnRails.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type ReactOnRailsCoreSpecificFunctions = Pick<
2222
| 'reactOnRailsStoreLoaded'
2323
| 'streamServerRenderedReactComponent'
2424
| 'serverRenderRSCReactComponent'
25+
| 'addAsyncPropsCapabilityToComponentProps'
2526
>;
2627

2728
export default function createReactOnRails(
@@ -76,6 +77,10 @@ export default function createReactOnRails(
7677
serverRenderRSCReactComponent(): any {
7778
throw new Error('serverRenderRSCReactComponent requires react-on-rails-pro package');
7879
},
80+
81+
addAsyncPropsCapabilityToComponentProps() {
82+
throw new Error('addAsyncPropsCapabilityToComponentProps requires react-on-rails-pro package');
83+
},
7984
};
8085

8186
// Type assertion is safe here because:

packages/react-on-rails/src/types/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ type RenderFunctionResult = RenderFunctionSyncResult | RenderFunctionAsyncResult
139139

140140
type StreamableComponentResult = ReactElement | Promise<ReactElement | string>;
141141

142+
type AsyncPropsManager = {
143+
getProp: (propName: string) => Promise<unknown>;
144+
setProp: (propName: string, propValue: unknown) => void;
145+
endStream: () => void;
146+
};
147+
142148
/**
143149
* Render-functions are used to create dynamic React components or server-rendered HTML with side effects.
144150
* They receive two arguments: props and railsContext.
@@ -355,6 +361,15 @@ export type RSCPayloadStreamInfo = {
355361

356362
export type RSCPayloadCallback = (streamInfo: RSCPayloadStreamInfo) => void;
357363

364+
export type WithAsyncProps<
365+
AsyncPropsType extends Record<string, unknown>,
366+
PropsType extends Record<string, unknown>,
367+
> = PropsType & {
368+
getReactOnRailsAsyncProp: <PropName extends keyof AsyncPropsType>(
369+
propName: PropName,
370+
) => Promise<AsyncPropsType[PropName]>;
371+
};
372+
358373
/** Contains the parts of the `ReactOnRails` API intended for internal use only. */
359374
export interface ReactOnRailsInternal extends ReactOnRails {
360375
/**
@@ -463,6 +478,19 @@ export interface ReactOnRailsInternal extends ReactOnRails {
463478
* Indicates if the RSC bundle is being used.
464479
*/
465480
isRSCBundle: boolean;
481+
/**
482+
* Adds the getAsyncProp function to the component props object
483+
* @returns An object containitng: the AsyncPropsManager and the component props after adding the getAsyncProp to it
484+
*/
485+
addAsyncPropsCapabilityToComponentProps: <
486+
AsyncPropsType extends Record<string, unknown>,
487+
PropsType extends Record<string, unknown>,
488+
>(
489+
props: PropsType,
490+
) => {
491+
asyncPropManager: AsyncPropsManager;
492+
props: WithAsyncProps<AsyncPropsType, PropsType>;
493+
};
466494
}
467495

468496
export type RenderStateHtml = FinalHtmlResult | Promise<FinalHtmlResult>;

react_on_rails_pro/packages/node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020

2121
ReactOnRails.clearHydratedStores();
2222
var usedProps = typeof props === 'undefined' ? {"helloWorldData":{"name":"Mr. Server Side Rendering","\u003cscript\u003ewindow.alert('xss1');\u003c/script\u003e":"\u003cscript\u003ewindow.alert(\"xss2\");\u003c/script\u003e"}} : props;
23+
24+
if (ReactOnRails.isRSCBundle) {
25+
var { props: propsWithAsyncProps, asyncPropsManager } = ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps);
26+
usedProps = propsWithAsyncProps;
27+
sharedExecutionContext.set("asyncPropsManager", asyncPropsManager);
28+
}
29+
2330
return ReactOnRails[ReactOnRails.isRSCBundle ? 'serverRenderRSCReactComponent' : 'streamServerRenderedReactComponent']({
2431
name: componentName,
2532
domNodeId: 'AsyncComponentsTreeForTesting-react-component-0',
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/// <reference types="react/experimental" />
2+
3+
import * as React from 'react';
4+
import { Suspense } from 'react';
5+
import { WithAsyncProps } from 'react-on-rails-pro';
6+
7+
type SyncPropsType = {
8+
name: string;
9+
age: number;
10+
description: string;
11+
};
12+
13+
type AsyncPropsType = {
14+
books: string[];
15+
researches: string[];
16+
};
17+
18+
type PropsType = WithAsyncProps<AsyncPropsType, SyncPropsType>;
19+
20+
const AsyncArrayComponent = async ({ items }: { items: Promise<string[]> }) => {
21+
const resolvedItems = await items;
22+
23+
return (
24+
<ol>
25+
{resolvedItems.map((value) => (
26+
<li key={value}>{value}</li>
27+
))}
28+
</ol>
29+
);
30+
};
31+
32+
const AsyncPropsComponent = ({ name, age, description, getReactOnRailsAsyncProp }: PropsType) => {
33+
const booksPromise = getReactOnRailsAsyncProp('books');
34+
const researchesPromise = getReactOnRailsAsyncProp('researches');
35+
36+
return (
37+
<div>
38+
<h1>Async Props Component</h1>
39+
<p>Name: {name}</p>
40+
<p>Age: {age}</p>
41+
<p>Description: {description}</p>
42+
43+
<h2>Books</h2>
44+
<Suspense fallback={<p>Loading Books...</p>}>
45+
<AsyncArrayComponent items={booksPromise} />
46+
</Suspense>
47+
48+
<h2>Researches</h2>
49+
<Suspense fallback={<p>Loading Researches...</p>}>
50+
<AsyncArrayComponent items={researchesPromise} />
51+
</Suspense>
52+
</div>
53+
);
54+
};
55+
56+
export default AsyncPropsComponent;

0 commit comments

Comments
 (0)