-
-
Notifications
You must be signed in to change notification settings - Fork 633
Expand file tree
/
Copy pathserverRender.ts
More file actions
128 lines (113 loc) · 4.22 KB
/
serverRender.ts
File metadata and controls
128 lines (113 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { createElement, type ReactElement } from 'react';
import type {
DehydratedRouterState,
TanStackHistory,
TanStackRouter,
TanStackSsrMatch,
TanStackSsrRouterState,
TanStackRouterOptions,
} from './types.ts';
import type { RailsContext } from 'react-on-rails/types';
import { normalizeSearch } from './utils.ts';
/**
* Builds a React element tree with RouterProvider and optional AppWrapper.
*/
function buildAppElement(
router: TanStackRouter,
RouterProvider: React.ComponentType<{ router: TanStackRouter }>,
AppWrapper: TanStackRouterOptions['AppWrapper'],
wrapperProps: Record<string, unknown>,
): ReactElement {
let app: ReactElement = createElement(RouterProvider, { router });
if (AppWrapper) {
const safeWrapperProps = { ...wrapperProps };
// eslint-disable-next-line no-underscore-dangle -- Internal hydration payload key should not reach user AppWrapper props.
delete safeWrapperProps.__tanstackRouterDehydratedState;
app = createElement(AppWrapper, safeWrapperProps, app);
}
return app;
}
function dehydrateSsrMatchId(id: string): string {
return id.split('/').join('\0');
}
function buildSsrMatch(match: unknown): TanStackSsrMatch | null {
if (!match || typeof match !== 'object') {
return null;
}
const candidate = match as Record<string, unknown>;
if (
typeof candidate.id !== 'string' ||
typeof candidate.updatedAt !== 'number' ||
typeof candidate.status !== 'string'
) {
return null;
}
const dehydratedMatch: TanStackSsrMatch = {
i: dehydrateSsrMatchId(candidate.id),
u: candidate.updatedAt,
s: candidate.status,
};
// eslint-disable-next-line no-underscore-dangle -- TanStack Router internal field name.
if (candidate.__beforeLoadContext !== undefined) {
// eslint-disable-next-line no-underscore-dangle -- TanStack Router internal field name.
dehydratedMatch.b = candidate.__beforeLoadContext;
}
if (candidate.loaderData !== undefined) {
dehydratedMatch.l = candidate.loaderData;
}
if (candidate.error !== undefined) {
dehydratedMatch.e = candidate.error;
}
if (candidate.ssr !== undefined) {
dehydratedMatch.ssr = candidate.ssr;
}
return dehydratedMatch;
}
function buildSsrRouterState(router: TanStackRouter): TanStackSsrRouterState {
const matches = Array.isArray(router.state.matches)
? router.state.matches.map(buildSsrMatch).filter((match): match is TanStackSsrMatch => match !== null)
: [];
return {
manifest: undefined,
lastMatchId: matches[matches.length - 1]?.i,
matches,
};
}
export interface TanStackServerRenderResult {
appElement: ReactElement;
dehydratedState: DehydratedRouterState;
}
/**
* Async server-side render for use with React on Rails Pro Node Renderer.
* Uses the public router.load() API — no private API workarounds needed.
*
* Requires: rendering_returns_promises = true in React on Rails Pro config.
*/
export async function serverRenderTanStackAppAsync(
options: TanStackRouterOptions,
props: Record<string, unknown>,
railsContext: RailsContext & { serverSide: true },
RouterProvider: React.ComponentType<{ router: TanStackRouter }>,
createMemoryHistory: (opts: { initialEntries: string[] }) => TanStackHistory,
): Promise<TanStackServerRenderResult> {
const router = options.createRouter();
const url = `${railsContext.pathname}${normalizeSearch(railsContext.search as string | null | undefined)}`;
const memoryHistory = createMemoryHistory({ initialEntries: [url] });
router.update({ history: memoryHistory });
// Async path uses router.load() public API, so no private store access is needed.
// No router.ssr flag is set here: React effects (including Transitioner's auto-load)
// do not execute during server-side renderToString, and router.dehydrate() does not
// depend on router.ssr.
await router.load();
const dehydratedState: DehydratedRouterState = {
url,
dehydratedRouter: typeof router.dehydrate === 'function' ? router.dehydrate() : null,
// Keep ssrRouter payload for compatibility and to restore server match data
// before first client render.
ssrRouter: buildSsrRouterState(router),
};
return {
appElement: buildAppElement(router, RouterProvider, options.AppWrapper, props),
dehydratedState,
};
}