Skip to content

Commit 66f9714

Browse files
feat: implement post-SSR hooks for enhanced server-side rendering control and update package dependencies
1 parent 8172e07 commit 66f9714

8 files changed

+61
-5
lines changed

node_package/src/ReactOnRails.client.ts

+6
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,12 @@ globalThis.ReactOnRails = {
190190
},
191191

192192
isRSCBundle: false,
193+
194+
addPostSSRHook(): void {
195+
throw new Error(
196+
'addPostSSRHook is not available in "react-on-rails/client". Import "react-on-rails" server-side.',
197+
);
198+
},
193199
};
194200

195201
globalThis.ReactOnRails.resetOptions();

node_package/src/ReactOnRails.node.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {
66
clearRSCPayloadStreams,
77
onRSCPayloadGenerated,
88
} from './RSCPayloadGenerator.ts';
9+
import { addPostSSRHook } from './postSSRHooks.ts';
910

1011
ReactOnRails.streamServerRenderedReactComponent = streamServerRenderedReactComponent;
1112
ReactOnRails.getRSCPayloadStream = getRSCPayloadStream;
1213
ReactOnRails.getRSCPayloadStreams = getRSCPayloadStreams;
1314
ReactOnRails.clearRSCPayloadStreams = clearRSCPayloadStreams;
1415
ReactOnRails.onRSCPayloadGenerated = onRSCPayloadGenerated;
16+
ReactOnRails.addPostSSRHook = addPostSSRHook;
1517

1618
export * from './ReactOnRails.full.ts';
1719
// eslint-disable-next-line no-restricted-exports -- see https://github.com/eslint/eslint/issues/15617

node_package/src/ReactOnRailsRSC.ts

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ReactOnRails from './ReactOnRails.full.ts';
66
import buildConsoleReplay from './buildConsoleReplay.ts';
77
import handleError from './handleError.ts';
88
import { convertToError, createResultObject } from './serverRenderUtils.ts';
9+
import { notifySSREnd, addPostSSRHook } from './postSSRHooks.ts';
910

1011
import {
1112
streamServerRenderedComponent,
@@ -61,6 +62,12 @@ const streamRenderRSCComponent = (
6162
const jsonResult = JSON.stringify(createResultObject(htmlResult, buildConsoleReplay(), renderState));
6263
return stringToStream(jsonResult);
6364
});
65+
66+
readableStream.on('end', () => {
67+
if (options.railsContext?.componentSpecificMetadata) {
68+
notifySSREnd(options.railsContext as RailsContextWithComponentSpecificMetadata);
69+
}
70+
});
6471
return readableStream;
6572
};
6673

@@ -72,6 +79,8 @@ ReactOnRails.serverRenderRSCReactComponent = (options: RSCRenderParams) => {
7279
}
7380
};
7481

82+
ReactOnRails.addPostSSRHook = addPostSSRHook;
83+
7584
ReactOnRails.isRSCBundle = true;
7685

7786
export * from './types/index.ts';

node_package/src/loadJsonFile.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ import * as fs from 'fs/promises';
44
type LoadedJsonFile = Record<string, unknown>;
55
const loadedJsonFiles = new Map<string, LoadedJsonFile>();
66

7-
export default async function loadJsonFile(fileName: string) {
7+
export default async function loadJsonFile<T extends LoadedJsonFile = LoadedJsonFile>(
8+
fileName: string,
9+
): Promise<T> {
810
// Asset JSON files are uploaded to node renderer.
911
// Renderer copies assets to the same place as the server-bundle.js and rsc-bundle.js.
1012
// Thus, the __dirname of this code is where we can find the manifest file.
1113
const filePath = path.resolve(__dirname, fileName);
1214
const loadedJsonFile = loadedJsonFiles.get(filePath);
1315
if (loadedJsonFile) {
14-
return loadedJsonFile;
16+
return loadedJsonFile as T;
1517
}
1618

1719
try {
18-
const file = JSON.parse(await fs.readFile(filePath, 'utf8')) as LoadedJsonFile;
20+
const file = JSON.parse(await fs.readFile(filePath, 'utf8')) as T;
1921
loadedJsonFiles.set(filePath, file);
2022
return file;
2123
} catch (error) {

node_package/src/postSSRHooks.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { RailsContextWithComponentSpecificMetadata } from "./types/index.ts";
2+
3+
type PostSSRHook = () => void;
4+
const postSSRHooks = new Map<string, PostSSRHook[]>();
5+
6+
export const addPostSSRHook = (
7+
railsContext: RailsContextWithComponentSpecificMetadata,
8+
hook: PostSSRHook,
9+
) => {
10+
const hooks = postSSRHooks.get(railsContext.componentSpecificMetadata.renderRequestId) || [];
11+
hooks.push(hook);
12+
postSSRHooks.set(railsContext.componentSpecificMetadata.renderRequestId, hooks);
13+
};
14+
15+
export const notifySSREnd = (railsContext: RailsContextWithComponentSpecificMetadata) => {
16+
const hooks = postSSRHooks.get(railsContext.componentSpecificMetadata.renderRequestId);
17+
if (hooks) {
18+
hooks.forEach((hook) => hook());
19+
}
20+
};

node_package/src/streamServerRenderedReactComponent.ts

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { renderToPipeableStream, PipeableStream } from './ReactDOMServer.cts';
1010
import { createResultObject, convertToError, validateComponent } from './serverRenderUtils.ts';
1111
import type { RenderParams, StreamRenderState, StreamableComponentResult } from './types/index.ts';
1212
import injectRSCPayload from './injectRSCPayload.ts';
13+
import { notifySSREnd } from './postSSRHooks.ts';
1314

1415
type BufferedEvent = {
1516
event: 'data' | 'error' | 'end';
@@ -187,6 +188,11 @@ const streamRenderReactComponent = (
187188
onError(e) {
188189
reportError(convertToError(e));
189190
},
191+
onAllReady() {
192+
if (railsContext.componentSpecificMetadata?.renderRequestId) {
193+
notifySSREnd(railsContext as RailsContextWithComponentSpecificMetadata);
194+
}
195+
},
190196
identifierPrefix: domNodeId,
191197
});
192198
})

node_package/src/types/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export type RailsContext = {
6262
}
6363
);
6464

65+
export type RailsContextWithComponentSpecificMetadata = RailsContext & {
66+
componentSpecificMetadata: {
67+
renderRequestId: string;
68+
};
69+
};
70+
6571
// not strictly what we want, see https://github.com/microsoft/TypeScript/issues/17867#issuecomment-323164375
6672
type AuthenticityHeaders = Record<string, string> & {
6773
'X-CSRF-Token': string | null;
@@ -291,6 +297,11 @@ export interface ReactOnRails {
291297
* @param otherHeaders Other headers
292298
*/
293299
authenticityHeaders(otherHeaders: Record<string, string>): AuthenticityHeaders;
300+
/**
301+
* Adds a post SSR hook to be called after the SSR has completed.
302+
* @param hook - The hook to be called after the SSR has completed.
303+
*/
304+
addPostSSRHook(railsContext: RailsContextWithComponentSpecificMetadata, hook: () => void): void;
294305
}
295306

296307
export type RSCPayloadStreamInfo = {

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
"eslint-plugin-prettier": "^5.2.3",
5353
"eslint-plugin-react": "^7.37.4",
5454
"eslint-plugin-react-hooks": "^5.2.0",
55-
"typescript-eslint": "^8.26.1",
5655
"globals": "^16.0.0",
5756
"jest": "^29.7.0",
5857
"jest-environment-jsdom": "^29.7.0",
@@ -68,7 +67,8 @@
6867
"react-on-rails-rsc": "git+https://github.com/shakacode/react_on_rails_rsc.git#add-support-for-generating-client-components-manifest-for-server-bundle",
6968
"redux": "^4.2.1",
7069
"ts-jest": "^29.2.5",
71-
"typescript": "^5.8.3"
70+
"typescript": "^5.8.3",
71+
"typescript-eslint": "^8.26.1"
7272
},
7373
"peerDependencies": {
7474
"react": ">= 16",

0 commit comments

Comments
 (0)