react-on-rails-pro 16.7.0-rc.1 → 16.7.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +83 -0
- package/lib/tanstack-router/clientHydrate.js +195 -82
- package/lib/tanstack-router/serverRender.js +8 -0
- package/lib/tanstack-router/types.d.ts +17 -0
- package/package.json +14 -14
package/LICENSE.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Licensing
|
|
2
|
+
|
|
3
|
+
This repository contains code under two different licenses:
|
|
4
|
+
|
|
5
|
+
- **Core**: MIT License (applies to most files)
|
|
6
|
+
- **Pro**: React on Rails Pro License (applies to specific directories)
|
|
7
|
+
|
|
8
|
+
## License Scope
|
|
9
|
+
|
|
10
|
+
### MIT Licensed Code
|
|
11
|
+
|
|
12
|
+
The following directories and all their contents are licensed under the **MIT License** (see full text below):
|
|
13
|
+
|
|
14
|
+
- `react_on_rails/` (entire directory, including lib/, spec/, sig/)
|
|
15
|
+
- `packages/react-on-rails/` (entire package)
|
|
16
|
+
- All other directories in this repository not explicitly listed as Pro-licensed
|
|
17
|
+
|
|
18
|
+
### Pro Licensed Code
|
|
19
|
+
|
|
20
|
+
The following directories and all their contents are licensed under the **React on Rails Pro License**:
|
|
21
|
+
|
|
22
|
+
- `packages/react-on-rails-pro/` (entire package)
|
|
23
|
+
- `packages/react-on-rails-pro-node-renderer/` (entire package)
|
|
24
|
+
- `react_on_rails_pro/` (entire directory)
|
|
25
|
+
|
|
26
|
+
See [REACT-ON-RAILS-PRO-LICENSE.md](./REACT-ON-RAILS-PRO-LICENSE.md) for complete Pro license terms.
|
|
27
|
+
|
|
28
|
+
**Important:** Pro-licensed code is included in this package but requires a valid React on Rails Pro subscription to use. Using Pro features without a valid license violates the React on Rails Pro License.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## MIT License
|
|
33
|
+
|
|
34
|
+
This license applies to all MIT-licensed code as defined above.
|
|
35
|
+
|
|
36
|
+
Copyright (c) 2017, 2018 Justin Gordon and ShakaCode
|
|
37
|
+
Copyright (c) 2015–2025 ShakaCode, LLC
|
|
38
|
+
|
|
39
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
40
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
41
|
+
in the Software without restriction, including without limitation the rights
|
|
42
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
43
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
44
|
+
furnished to do so, subject to the following conditions:
|
|
45
|
+
|
|
46
|
+
The above copyright notice and this permission notice shall be included in
|
|
47
|
+
all copies or substantial portions of the Software.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Disclaimer
|
|
52
|
+
|
|
53
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
54
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
55
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
56
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
57
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
58
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
59
|
+
SOFTWARE.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## React on Rails Pro License
|
|
64
|
+
|
|
65
|
+
For Pro-licensed code (as defined in the "License Scope" section above), see:
|
|
66
|
+
[REACT-ON-RAILS-PRO-LICENSE.md](./REACT-ON-RAILS-PRO-LICENSE.md)
|
|
67
|
+
|
|
68
|
+
**Key Points:**
|
|
69
|
+
|
|
70
|
+
- Pro features require a valid React on Rails Pro subscription for production use
|
|
71
|
+
- Free use is permitted for educational, personal, and non-production purposes
|
|
72
|
+
- Modifying MIT-licensed interface files is permitted under MIT terms
|
|
73
|
+
- However, using those modifications to access Pro features without a valid license violates the Pro License
|
|
74
|
+
|
|
75
|
+
### License Validation Mechanisms
|
|
76
|
+
|
|
77
|
+
**License validation mechanisms** include but are not limited to:
|
|
78
|
+
|
|
79
|
+
- Runtime checks for valid Pro subscriptions
|
|
80
|
+
- Authentication systems in `react_on_rails/lib/react_on_rails/utils.rb` and Pro TypeScript modules
|
|
81
|
+
- The `react_on_rails_pro?` method and `rorPro` field generation
|
|
82
|
+
|
|
83
|
+
While MIT-licensed code may be modified under MIT terms, using such modifications to access Pro features without a valid license violates the React on Rails Pro License.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
/* eslint-disable import/prefer-default-export, no-underscore-dangle */
|
|
3
3
|
const { createElement, useEffect, useRef } = React;
|
|
4
|
+
const sharedHydrationInitStates = new WeakMap();
|
|
4
5
|
function extractDehydratedData(dehydratedRouter) {
|
|
5
6
|
if (!dehydratedRouter || typeof dehydratedRouter !== 'object') {
|
|
6
7
|
return undefined;
|
|
@@ -33,6 +34,77 @@ function preloadMatchedRouteChunks(router, matches) {
|
|
|
33
34
|
console.error('react-on-rails-pro/tanstack-router: Error preloading matched route chunks:', error);
|
|
34
35
|
});
|
|
35
36
|
}
|
|
37
|
+
// Each guard repeats the matchRoutes check so TypeScript narrows correctly
|
|
38
|
+
// when either hydration path is used independently.
|
|
39
|
+
function hasLegacyHydrationStore(router) {
|
|
40
|
+
return typeof router.matchRoutes === 'function' && typeof router.__store?.setState === 'function';
|
|
41
|
+
}
|
|
42
|
+
function hasStoresHydrationApi(router) {
|
|
43
|
+
const { stores } = router;
|
|
44
|
+
return (typeof router.matchRoutes === 'function' &&
|
|
45
|
+
typeof stores?.status?.set === 'function' &&
|
|
46
|
+
typeof stores?.resolvedLocation?.set === 'function' &&
|
|
47
|
+
typeof stores?.setMatches === 'function');
|
|
48
|
+
}
|
|
49
|
+
function hasHydrationInternals(router) {
|
|
50
|
+
// The ordering here is load-bearing: legacy `__store` wins when both APIs
|
|
51
|
+
// are present, so routers exposing both during a TanStack upgrade keep
|
|
52
|
+
// taking the well-tested __store.setState path until the legacy API is
|
|
53
|
+
// removed. applyHydrationMatches mirrors this preference at the call site.
|
|
54
|
+
return hasLegacyHydrationStore(router) || hasStoresHydrationApi(router);
|
|
55
|
+
}
|
|
56
|
+
function throwMissingHydrationInternals() {
|
|
57
|
+
throw new Error('react-on-rails-pro/tanstack-router: router.matchRoutes() and router.__store.setState() or ' +
|
|
58
|
+
'router.stores.setMatches() are required but not available. Ensure @tanstack/react-router ' +
|
|
59
|
+
'>=1.139.0 <2.0.0 is installed; older 1.x routers expose __store.setState(), while newer ' +
|
|
60
|
+
'1.x routers expose stores.setMatches().');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Apply server-rendered route matches to the router's internal hydration state.
|
|
64
|
+
*
|
|
65
|
+
* Precondition: callers must validate the router with `hasHydrationInternals(router)`
|
|
66
|
+
* before invoking this; the union parameter type encodes that contract so TypeScript
|
|
67
|
+
* enforces it at the call site.
|
|
68
|
+
*/
|
|
69
|
+
function applyHydrationMatches(router, matches) {
|
|
70
|
+
if (hasLegacyHydrationStore(router)) {
|
|
71
|
+
router.__store.setState((s) => ({
|
|
72
|
+
...s,
|
|
73
|
+
status: 'idle',
|
|
74
|
+
resolvedLocation: s.location,
|
|
75
|
+
matches,
|
|
76
|
+
}));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Legacy path didn't match, so the union narrows to TanStackRouterStoresHydrationInternals.
|
|
80
|
+
const applyStoresUpdate = () => {
|
|
81
|
+
router.stores.status.set('idle');
|
|
82
|
+
// The freshly-created router has not rendered or awaited work yet, so
|
|
83
|
+
// router.state.location matches the legacy __store updater's s.location.
|
|
84
|
+
// Invariant: router.update({ history }) does not mutate state.location synchronously;
|
|
85
|
+
// if that ever changes, this path and the legacy __store path diverge.
|
|
86
|
+
router.stores.resolvedLocation.set(router.state.location);
|
|
87
|
+
router.stores.setMatches(matches);
|
|
88
|
+
};
|
|
89
|
+
if (typeof router.batch === 'function') {
|
|
90
|
+
router.batch(applyStoresUpdate);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Without router.batch, the stores API cannot make these writes atomic like
|
|
94
|
+
// legacy __store.setState(); this render-phase update runs before
|
|
95
|
+
// RouterProvider subscribes, so hydration still starts from the final state.
|
|
96
|
+
// NOTE: correctness here depends on RouterProvider not subscribing to stores
|
|
97
|
+
// during synchronous render. Re-validate this path on TanStack Router major upgrades.
|
|
98
|
+
// In practice router.batch is present in the supported range (>=1.139.0), so this
|
|
99
|
+
// branch is a defensive belt-and-suspenders fallback rather than an expected runtime
|
|
100
|
+
// path — the dev warning below should not fire on a correctly pinned dependency.
|
|
101
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
102
|
+
console.warn('react-on-rails-pro/tanstack-router: router.batch is unavailable; stores hydration writes ' +
|
|
103
|
+
'are not atomic. Upgrade @tanstack/react-router to a version that exposes router.batch ' +
|
|
104
|
+
'for safer hydration.');
|
|
105
|
+
}
|
|
106
|
+
applyStoresUpdate();
|
|
107
|
+
}
|
|
36
108
|
/**
|
|
37
109
|
* Converts a dehydrated match ID (using \0 separator) back to the standard
|
|
38
110
|
* route ID format (using / separator) used by matchRoutes().
|
|
@@ -74,9 +146,7 @@ function applyDehydratedMatchData(matches, ssrMatches, onMissingSsrMatch) {
|
|
|
74
146
|
return m;
|
|
75
147
|
});
|
|
76
148
|
}
|
|
77
|
-
function TanStackHydrationApp({ options, incomingProps,
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Required by TanStackHydrationAppProps interface.
|
|
79
|
-
railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
|
|
149
|
+
function TanStackHydrationApp({ options, incomingProps, railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
|
|
80
150
|
const dehydratedState = incomingProps.__tanstackRouterDehydratedState;
|
|
81
151
|
const hasSsrPayload = dehydratedState != null;
|
|
82
152
|
const hasDehydratedRouter = dehydratedState?.dehydratedRouter !== undefined && dehydratedState.dehydratedRouter !== null;
|
|
@@ -113,91 +183,122 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
|
|
|
113
183
|
// block completes. If React discards this render (StrictMode/concurrency),
|
|
114
184
|
// the discarded router instance is dropped and a fresh instance is created
|
|
115
185
|
// and initialized on the next render.
|
|
186
|
+
//
|
|
187
|
+
// Cross-mount invariant: sharedHydrationInitStates dedupes per-router
|
|
188
|
+
// side effects (loadRouteChunk, __store.setState, options.hydrate) across
|
|
189
|
+
// React 18 StrictMode's double-render-with-fresh-hooks behavior — which
|
|
190
|
+
// resets useRef on each pass, so this `routerRef.current === null` guard
|
|
191
|
+
// alone fires twice when options.createRouter returns the same instance.
|
|
116
192
|
const router = options.createRouter();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// throwing loadPromise (which would cause Suspense suspension).
|
|
146
|
-
const rawMatches = hydrationRouter.matchRoutes(hydrationRouter.state.location);
|
|
147
|
-
routeChunkPreloadPromiseRef.current = preloadMatchedRouteChunks(router, rawMatches);
|
|
148
|
-
const ssrMatches = dehydratedState?.ssrRouter?.matches;
|
|
149
|
-
const matches = ssrMatches?.length
|
|
150
|
-
? applyDehydratedMatchData(rawMatches, ssrMatches, warnMissingSsrMatch)
|
|
151
|
-
: rawMatches.map((match) => {
|
|
152
|
-
const m = match;
|
|
153
|
-
if (m.status === 'pending') {
|
|
154
|
-
warnMissingSsrMatch(m);
|
|
155
|
-
return { ...m, status: 'success' };
|
|
156
|
-
}
|
|
157
|
-
return m;
|
|
158
|
-
});
|
|
159
|
-
// Render-phase store injection is required for hydration parity: this
|
|
160
|
-
// must happen before the first RouterProvider render.
|
|
161
|
-
hydrationRouter.__store.setState((s) => ({
|
|
162
|
-
...s,
|
|
163
|
-
status: 'idle',
|
|
164
|
-
resolvedLocation: s.location,
|
|
165
|
-
matches,
|
|
166
|
-
}));
|
|
167
|
-
// Set SSR flag so the Transitioner skips its initial router.load() call,
|
|
168
|
-
// preventing a state update during hydration that would cause a mismatch.
|
|
169
|
-
// The shape matches TanStack Router's internal $_TSR hydration contract
|
|
170
|
-
// (the Transitioner only checks truthiness).
|
|
171
|
-
// Preserve user-set values from createRouter() (e.g. TanStack Start).
|
|
172
|
-
if (!router.ssr) {
|
|
173
|
-
router.ssr = { manifest: undefined };
|
|
174
|
-
didSetSsrFlagRef.current = true;
|
|
193
|
+
const cachedInit = sharedHydrationInitStates.get(router);
|
|
194
|
+
if (cachedInit) {
|
|
195
|
+
// Same router instance was already initialized by a discarded render
|
|
196
|
+
// (or prior mount). Reattach the pending preload/hydrate promises to
|
|
197
|
+
// this mount's refs so the post-hydration effect awaits the original
|
|
198
|
+
// work; restore didSetSsrFlag so cleanup correctly clears router.ssr.
|
|
199
|
+
routeChunkPreloadPromiseRef.current = cachedInit.routeChunkPreloadPromise;
|
|
200
|
+
hydrationCallbackPromiseRef.current = cachedInit.hydrationCallbackPromise;
|
|
201
|
+
didSetSsrFlagRef.current = cachedInit.didSetSsrFlag;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Set browser history for client-side navigation
|
|
205
|
+
const browserHistory = createBrowserHistory();
|
|
206
|
+
// Snapshot state.location before router.update so the dev-mode assertion
|
|
207
|
+
// below can verify the equivalence the stores hydration path relies on
|
|
208
|
+
// (see applyHydrationMatches: router.stores.resolvedLocation.set
|
|
209
|
+
// assumes router.update does not mutate state.location synchronously).
|
|
210
|
+
const locationBeforeUpdate = process.env.NODE_ENV !== 'production' ? router.state.location : undefined;
|
|
211
|
+
router.update({ history: browserHistory });
|
|
212
|
+
if (process.env.NODE_ENV !== 'production' &&
|
|
213
|
+
hasStoresHydrationApi(router) &&
|
|
214
|
+
// Identity check only: an in-place mutation of the same location object
|
|
215
|
+
// would slip past this guard. The dev warning is best-effort.
|
|
216
|
+
router.state.location !== locationBeforeUpdate) {
|
|
217
|
+
console.warn('react-on-rails-pro/tanstack-router: router.update({ history }) mutated router.state.location ' +
|
|
218
|
+
'synchronously. The stores hydration path writes router.state.location to ' +
|
|
219
|
+
'router.stores.resolvedLocation, which would diverge from the legacy __store path that ' +
|
|
220
|
+
'snapshots the pre-update s.location. File an issue with your @tanstack/react-router version.');
|
|
175
221
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
222
|
+
// Only apply SSR hydration when a server payload exists.
|
|
223
|
+
// Client-only renders (prerender: false) must not set router.ssr or
|
|
224
|
+
// inject matches — the Transitioner handles initial loading for those.
|
|
225
|
+
if (hasSsrPayload) {
|
|
226
|
+
if (process.env.NODE_ENV === 'development' && !didWarnPrivateInternalsRef.current) {
|
|
227
|
+
didWarnPrivateInternalsRef.current = true;
|
|
228
|
+
console.warn('react-on-rails-pro/tanstack-router: Hydration uses TanStack Router private internals ' +
|
|
229
|
+
'(matchRoutes, __store/stores, loadRouteChunk, looseRoutesById). Keep @tanstack/react-router ' +
|
|
230
|
+
'within the supported range (>=1.139.0 <2.0.0) and run integration tests when upgrading.');
|
|
185
231
|
}
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
router.hydrate(dehydratedState.dehydratedRouter);
|
|
232
|
+
// Validate internal APIs before using them.
|
|
233
|
+
if (!hasHydrationInternals(router)) {
|
|
234
|
+
throwMissingHydrationInternals();
|
|
190
235
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
236
|
+
// Synchronously inject route matches to match server-rendered output.
|
|
237
|
+
// The server fully loads routes (via router.load()) before rendering, so
|
|
238
|
+
// all matches are resolved. We replicate this on the client so the initial
|
|
239
|
+
// render produces the same component tree as the server HTML.
|
|
240
|
+
//
|
|
241
|
+
// When ssrRouter match data is available (from serverRenderTanStackAppAsync),
|
|
242
|
+
// we apply loaderData, beforeLoadContext, status, etc. from the server payload
|
|
243
|
+
// so routes that render from loader results can hydrate correctly.
|
|
244
|
+
// Otherwise we override 'pending' to 'success' to prevent MatchInner from
|
|
245
|
+
// throwing loadPromise (which would cause Suspense suspension).
|
|
246
|
+
const rawMatches = router.matchRoutes(router.state.location);
|
|
247
|
+
routeChunkPreloadPromiseRef.current = preloadMatchedRouteChunks(router, rawMatches);
|
|
248
|
+
const ssrMatches = dehydratedState?.ssrRouter?.matches;
|
|
249
|
+
const matches = ssrMatches?.length
|
|
250
|
+
? applyDehydratedMatchData(rawMatches, ssrMatches, warnMissingSsrMatch)
|
|
251
|
+
: rawMatches.map((match) => {
|
|
252
|
+
const m = match;
|
|
253
|
+
if (m.status === 'pending') {
|
|
254
|
+
warnMissingSsrMatch(m);
|
|
255
|
+
return { ...m, status: 'success' };
|
|
256
|
+
}
|
|
257
|
+
return m;
|
|
258
|
+
});
|
|
259
|
+
// Render-phase store injection is required for hydration parity: this
|
|
260
|
+
// must happen before the first RouterProvider render.
|
|
261
|
+
applyHydrationMatches(router, matches);
|
|
262
|
+
// Set SSR flag so the Transitioner skips its initial router.load() call,
|
|
263
|
+
// preventing a state update during hydration that would cause a mismatch.
|
|
264
|
+
// The shape matches TanStack Router's internal $_TSR hydration contract
|
|
265
|
+
// (the Transitioner only checks truthiness).
|
|
266
|
+
// Preserve user-set values from createRouter() (e.g. TanStack Start).
|
|
267
|
+
if (!router.ssr) {
|
|
268
|
+
router.ssr = { manifest: undefined };
|
|
269
|
+
didSetSsrFlagRef.current = true;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
// Run user-defined hydration callback for custom dehydratedData
|
|
273
|
+
// (for example external query/cache payloads), matching TanStack
|
|
274
|
+
// Router's ssr-client behavior.
|
|
275
|
+
if (typeof router.options?.hydrate === 'function') {
|
|
276
|
+
const hydrationResult = router.options.hydrate(extractDehydratedData(dehydratedState?.dehydratedRouter));
|
|
277
|
+
// Let async hydration failures reject so we do not continue into
|
|
278
|
+
// router.load() with partially hydrated client state.
|
|
279
|
+
hydrationCallbackPromiseRef.current = Promise.resolve(hydrationResult).then(() => undefined);
|
|
280
|
+
}
|
|
281
|
+
// Backward-compatibility hook: if user router exposes router.hydrate(),
|
|
282
|
+
// invoke it with the full dehydrated router payload.
|
|
283
|
+
if (hasDehydratedRouter && typeof router.hydrate === 'function') {
|
|
284
|
+
router.hydrate(dehydratedState.dehydratedRouter);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
// If render-phase hydration throws, clear only the temporary SSR flag
|
|
289
|
+
// created by this module so retries are not blocked.
|
|
290
|
+
if (didSetSsrFlagRef.current) {
|
|
291
|
+
router.ssr = undefined;
|
|
292
|
+
didSetSsrFlagRef.current = false;
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
198
295
|
}
|
|
199
|
-
throw error;
|
|
200
296
|
}
|
|
297
|
+
sharedHydrationInitStates.set(router, {
|
|
298
|
+
routeChunkPreloadPromise: routeChunkPreloadPromiseRef.current,
|
|
299
|
+
hydrationCallbackPromise: hydrationCallbackPromiseRef.current,
|
|
300
|
+
didSetSsrFlag: didSetSsrFlagRef.current,
|
|
301
|
+
});
|
|
201
302
|
}
|
|
202
303
|
routerRef.current = router;
|
|
203
304
|
}
|
|
@@ -267,6 +368,14 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
|
|
|
267
368
|
if (latestEffectRunIdRef.current === effectRunId && didSetSsrFlagRef.current) {
|
|
268
369
|
router.ssr = undefined;
|
|
269
370
|
didSetSsrFlagRef.current = false;
|
|
371
|
+
// Keep sharedHydrationInitStates in sync so a later mount of the
|
|
372
|
+
// same cached router doesn't restore a stale didSetSsrFlag=true and
|
|
373
|
+
// trigger the dev sanity-check warning below on a router whose ssr
|
|
374
|
+
// flag was already cleared correctly.
|
|
375
|
+
const cached = sharedHydrationInitStates.get(router);
|
|
376
|
+
if (cached) {
|
|
377
|
+
cached.didSetSsrFlag = false;
|
|
378
|
+
}
|
|
270
379
|
}
|
|
271
380
|
});
|
|
272
381
|
return () => {
|
|
@@ -278,6 +387,10 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
|
|
|
278
387
|
if (latestEffectRunIdRef.current === effectRunId && didSetSsrFlagRef.current) {
|
|
279
388
|
router.ssr = undefined;
|
|
280
389
|
didSetSsrFlagRef.current = false;
|
|
390
|
+
const cached = sharedHydrationInitStates.get(router);
|
|
391
|
+
if (cached) {
|
|
392
|
+
cached.didSetSsrFlag = false;
|
|
393
|
+
}
|
|
281
394
|
}
|
|
282
395
|
});
|
|
283
396
|
const cancellableRouter = router;
|
|
@@ -2,6 +2,14 @@ import { createElement } from 'react';
|
|
|
2
2
|
import { normalizeSearch } from "./utils.js";
|
|
3
3
|
/**
|
|
4
4
|
* Builds a React element tree with RouterProvider and optional AppWrapper.
|
|
5
|
+
*
|
|
6
|
+
* No <Suspense> boundary is inserted here. The client hydration tree renders
|
|
7
|
+
* RouterProvider directly without a wrapping <Suspense>, so introducing one
|
|
8
|
+
* on the server would emit `<!--$-->`/`<!--/$-->` markers (React 19's
|
|
9
|
+
* `renderToString` emits these for every Suspense boundary, even
|
|
10
|
+
* non-suspended ones) and break hydration parity. If RouterProvider suspends
|
|
11
|
+
* during SSR, React's own `renderToString` throws synchronously — that is
|
|
12
|
+
* already a loud failure mode and does not need a custom guard.
|
|
5
13
|
*/
|
|
6
14
|
function buildAppElement(router, RouterProvider, AppWrapper, wrapperProps) {
|
|
7
15
|
let app = createElement(RouterProvider, { router });
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Shape of a writable TanStack store atom. Used by the modern `router.stores`
|
|
4
|
+
* API exposed on TanStackRouter below.
|
|
5
|
+
*
|
|
6
|
+
* `set` is declared as an overload to match the upstream `@tanstack/store`
|
|
7
|
+
* `Atom.set` signature (which is an overload, not a union parameter); a union
|
|
8
|
+
* parameter would not be structurally assignable from the upstream type.
|
|
9
|
+
*/
|
|
10
|
+
export type TanStackRouterWritableStore<TValue = unknown> = {
|
|
11
|
+
set: ((value: TValue) => void) & ((updater: (prev: TValue) => TValue) => void);
|
|
12
|
+
};
|
|
2
13
|
/**
|
|
3
14
|
* Minimal type for TanStack Router instance.
|
|
4
15
|
* We use this instead of importing @tanstack/react-router directly
|
|
@@ -13,6 +24,12 @@ export interface TanStackRouter {
|
|
|
13
24
|
__store?: {
|
|
14
25
|
setState: (updater: (s: Record<string, unknown>) => Record<string, unknown>) => void;
|
|
15
26
|
};
|
|
27
|
+
stores?: {
|
|
28
|
+
status: TanStackRouterWritableStore<'idle' | 'pending'>;
|
|
29
|
+
resolvedLocation: TanStackRouterWritableStore<TanStackRouter['state']['location']>;
|
|
30
|
+
setMatches: (nextMatches: unknown[]) => void;
|
|
31
|
+
};
|
|
32
|
+
batch?: (callback: () => void) => void;
|
|
16
33
|
looseRoutesById?: Record<string, unknown>;
|
|
17
34
|
loadRouteChunk?: (route: unknown) => Promise<unknown>;
|
|
18
35
|
state: {
|
package/package.json
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-on-rails-pro",
|
|
3
|
-
"version": "16.7.0-rc.
|
|
3
|
+
"version": "16.7.0-rc.3",
|
|
4
4
|
"description": "React on Rails Pro package with React Server Components support",
|
|
5
5
|
"main": "lib/ReactOnRails.full.js",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "pnpm run clean && tsc",
|
|
9
|
-
"build-watch": "pnpm run clean && tsc --watch",
|
|
10
|
-
"clean": "rm -rf ./lib",
|
|
11
|
-
"test": "pnpm run test:non-rsc && pnpm run test:rsc",
|
|
12
|
-
"test:non-rsc": "jest tests --testPathIgnorePatterns=\".*(RSC|stream|registerServerComponent|serverRenderReactComponent|SuspenseHydration).*\"",
|
|
13
|
-
"test:rsc": "node scripts/check-react-version.cjs || NODE_CONDITIONS=react-server jest tests/*.rsc.test.*",
|
|
14
|
-
"type-check": "tsc --noEmit --noErrorTruncation",
|
|
15
|
-
"prepare": "[ -f lib/ReactOnRails.full.js ] || (rm -rf ./lib && tsc)",
|
|
16
|
-
"prepublishOnly": "pnpm run build"
|
|
17
|
-
},
|
|
18
7
|
"repository": {
|
|
19
8
|
"type": "git",
|
|
20
9
|
"url": "git+https://github.com/shakacode/react_on_rails.git",
|
|
@@ -59,7 +48,7 @@
|
|
|
59
48
|
"./ServerComponentFetchError": "./lib/ServerComponentFetchError.js"
|
|
60
49
|
},
|
|
61
50
|
"dependencies": {
|
|
62
|
-
"react-on-rails": "
|
|
51
|
+
"react-on-rails": "16.7.0-rc.3"
|
|
63
52
|
},
|
|
64
53
|
"peerDependencies": {
|
|
65
54
|
"react": ">= 16",
|
|
@@ -85,10 +74,21 @@
|
|
|
85
74
|
},
|
|
86
75
|
"homepage": "https://reactonrails.com/docs/pro/",
|
|
87
76
|
"devDependencies": {
|
|
77
|
+
"@tanstack/react-router": "1.163.3",
|
|
78
|
+
"@tanstack/store": "0.9.1",
|
|
88
79
|
"@types/mock-fs": "^4.13.4",
|
|
89
80
|
"mock-fs": "^5.5.0",
|
|
90
81
|
"react": "^19.0.3",
|
|
91
82
|
"react-dom": "^19.0.3",
|
|
92
83
|
"react-on-rails-rsc": "^19.0.4"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "pnpm run clean && tsc",
|
|
87
|
+
"build-watch": "pnpm run clean && tsc --watch",
|
|
88
|
+
"clean": "rm -rf ./lib",
|
|
89
|
+
"test": "pnpm run test:non-rsc && pnpm run test:rsc",
|
|
90
|
+
"test:non-rsc": "jest tests --testPathIgnorePatterns=\".*(RSC|stream|registerServerComponent|serverRenderReactComponent|SuspenseHydration).*\"",
|
|
91
|
+
"test:rsc": "node scripts/check-react-version.cjs || NODE_CONDITIONS=react-server jest tests/*.rsc.test.*",
|
|
92
|
+
"type-check": "tsc --noEmit --noErrorTruncation"
|
|
93
93
|
}
|
|
94
|
-
}
|
|
94
|
+
}
|