react-on-rails-pro 16.7.0-rc.0 → 16.7.0-rc.2

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/README.md CHANGED
@@ -97,4 +97,4 @@ See the [full installation guide](https://reactonrails.com/docs/pro/installation
97
97
 
98
98
  ## License
99
99
 
100
- Commercial software. No license required for evaluation, development, testing, or CI/CD. A paid license is required for production deployments. Contact [justin@shakacode.com](mailto:justin@shakacode.com) for licensing.
100
+ Commercial software. No license required for evaluation, development, testing, or CI/CD. A paid license is required for production deployments. Contact [ShakaCode](https://pro.reactonrails.com/contact) for licensing.
@@ -1,9 +1,9 @@
1
- import { type ReactElement } from 'react';
1
+ import * as React from 'react';
2
2
  import type { TanStackHistory, TanStackRouter, TanStackRouterOptions } from './types.ts';
3
3
  import type { RailsContext } from 'react-on-rails/types';
4
4
  export declare function clientHydrateTanStackApp(options: TanStackRouterOptions, props: Record<string, unknown>, railsContext: RailsContext & {
5
5
  serverSide: false;
6
6
  }, RouterProvider: React.ComponentType<{
7
7
  router: TanStackRouter;
8
- }>, createBrowserHistory: () => TanStackHistory): ReactElement;
8
+ }>, createBrowserHistory: () => TanStackHistory): React.ReactElement;
9
9
  //# sourceMappingURL=clientHydrate.d.ts.map
@@ -1,17 +1,6 @@
1
- import { Suspense, createElement, useEffect, useRef } from 'react';
2
- function RouteChunkPreloadGate({ preloadPromise, preloadSettledRef, isHydrating, children, }) {
3
- // During SSR hydration (first render), skip the suspension gate to avoid a
4
- // hydration mismatch: the server rendered RouterProvider content directly,
5
- // so throwing a promise here would cause Suspense to render the null fallback
6
- // instead of matching the server HTML. After hydration completes (the
7
- // post-mount effect sets didTriggerPostHydrationLoadRef), the gate activates
8
- // normally for any subsequent re-renders.
9
- if (!isHydrating && preloadPromise && !preloadSettledRef.current) {
10
- // eslint-disable-next-line @typescript-eslint/only-throw-error -- Suspense boundaries intentionally suspend on thrown Promise.
11
- throw preloadPromise;
12
- }
13
- return children;
14
- }
1
+ import * as React from 'react';
2
+ /* eslint-disable import/prefer-default-export, no-underscore-dangle */
3
+ const { createElement, useEffect, useRef } = React;
15
4
  function extractDehydratedData(dehydratedRouter) {
16
5
  if (!dehydratedRouter || typeof dehydratedRouter !== 'object') {
17
6
  return undefined;
@@ -94,8 +83,10 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
94
83
  const routerRef = useRef(null);
95
84
  const didTriggerPostHydrationLoadRef = useRef(false);
96
85
  const didSetSsrFlagRef = useRef(false);
86
+ const latestEffectRunIdRef = useRef(0); // 0 = no post-hydration effect run yet.
87
+ // Set during render-phase SSR init; awaited in runPostHydrationLoad before
88
+ // router.load() so post-hydration navigation waits for matched lazy chunks.
97
89
  const routeChunkPreloadPromiseRef = useRef(null);
98
- const routeChunkPreloadSettledRef = useRef(true);
99
90
  const hydrationCallbackPromiseRef = useRef(null);
100
91
  const didWarnPrivateInternalsRef = useRef(false);
101
92
  const warnedMissingSsrMatchIdsRef = useRef(new Set());
@@ -154,15 +145,6 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
154
145
  // throwing loadPromise (which would cause Suspense suspension).
155
146
  const rawMatches = hydrationRouter.matchRoutes(hydrationRouter.state.location);
156
147
  routeChunkPreloadPromiseRef.current = preloadMatchedRouteChunks(router, rawMatches);
157
- if (routeChunkPreloadPromiseRef.current) {
158
- routeChunkPreloadSettledRef.current = false;
159
- void routeChunkPreloadPromiseRef.current.finally(() => {
160
- routeChunkPreloadSettledRef.current = true;
161
- });
162
- }
163
- else {
164
- routeChunkPreloadSettledRef.current = true;
165
- }
166
148
  const ssrMatches = dehydratedState?.ssrRouter?.matches;
167
149
  const matches = ssrMatches?.length
168
150
  ? applyDehydratedMatchData(rawMatches, ssrMatches, warnMissingSsrMatch)
@@ -230,6 +212,21 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
230
212
  return undefined;
231
213
  }
232
214
  didTriggerPostHydrationLoadRef.current = true;
215
+ const effectRunId = latestEffectRunIdRef.current + 1;
216
+ latestEffectRunIdRef.current = effectRunId;
217
+ // Dev-mode sanity check: router.ssr should still hold the value we wrote
218
+ // during render-phase init. Our window-safety argument (parent re-renders
219
+ // are safe because router.ssr blocks Transitioner navigation) depends on
220
+ // a private TanStack Router API. If that API is renamed or removed in a
221
+ // future version, this warning surfaces the breakage before it manifests
222
+ // as a hard-to-diagnose navigation race.
223
+ if (process.env.NODE_ENV === 'development' && didSetSsrFlagRef.current && router.ssr == null) {
224
+ console.warn('react-on-rails-pro/tanstack-router: router.ssr was unexpectedly ' +
225
+ 'cleared between render-phase init and the post-hydration effect. ' +
226
+ 'TanStack Router\'s private "ssr" API may have changed — verify ' +
227
+ '@tanstack/react-router is within the supported range ' +
228
+ '(>=1.139.0 <2.0.0).');
229
+ }
233
230
  let cancelled = false;
234
231
  const runPostHydrationLoad = async () => {
235
232
  if (hydrationCallbackPromiseRef.current) {
@@ -244,9 +241,10 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
244
241
  return;
245
242
  }
246
243
  }
247
- if (cancelled) {
248
- return;
249
- }
244
+ // No final cancellation check is needed for the no-await fast path:
245
+ // without pending hydration or preload promises, cleanup cannot run between
246
+ // the checks above and this call. If unmount happens after router.load()
247
+ // starts, the cleanup's cancelLoad() call handles the in-flight load.
250
248
  await router.load();
251
249
  };
252
250
  void runPostHydrationLoad()
@@ -256,45 +254,51 @@ railsContext: _railsContext, RouterProvider, createBrowserHistory, }) {
256
254
  }
257
255
  })
258
256
  .finally(() => {
259
- // Always clear temporary router.ssr set by this module, regardless of
260
- // cancellation state. In React 18 StrictMode, the effect cleanup sets
261
- // cancelled=true and didTriggerPostHydrationLoadRef prevents re-trigger
262
- // on re-mount — if we skip cleanup here the SSR flag stays set
263
- // permanently, blocking the Transitioner from ever calling router.load().
264
- // The didSetSsrFlagRef guard ensures we only clear values this module
265
- // created, preserving user-provided router.ssr from createRouter().
266
- if (didSetSsrFlagRef.current) {
257
+ // Invariant: temporary router.ssr is cleared by exactly one path:
258
+ // 1. this finally block after the post-hydration load settles/cancels;
259
+ // 2. this effect cleanup's deferred continuation when unmount happens
260
+ // before that settle.
261
+ // didSetSsrFlagRef is the shared latch, preserving user-provided
262
+ // router.ssr from createRouter(). latestEffectRunIdRef prevents stale
263
+ // StrictMode passive-effect finally blocks from racing a remount. Today
264
+ // React runs passive cleanup/setup inside one synchronous
265
+ // flushPassiveEffects() call, so queued promise continuations cannot
266
+ // drain between the cleanup and the re-setup.
267
+ if (latestEffectRunIdRef.current === effectRunId && didSetSsrFlagRef.current) {
267
268
  router.ssr = undefined;
268
269
  didSetSsrFlagRef.current = false;
269
270
  }
270
271
  });
271
272
  return () => {
272
273
  cancelled = true;
273
- if (didSetSsrFlagRef.current) {
274
- router.ssr = undefined;
275
- didSetSsrFlagRef.current = false;
276
- }
274
+ didTriggerPostHydrationLoadRef.current = false;
275
+ // Defer the unmount clear so React 18 StrictMode's passive cleanup/setup
276
+ // replay can increment latestEffectRunIdRef before this continuation runs.
277
+ void Promise.resolve().then(() => {
278
+ if (latestEffectRunIdRef.current === effectRunId && didSetSsrFlagRef.current) {
279
+ router.ssr = undefined;
280
+ didSetSsrFlagRef.current = false;
281
+ }
282
+ });
277
283
  const cancellableRouter = router;
278
284
  if (typeof cancellableRouter.cancelLoad === 'function') {
279
285
  cancellableRouter.cancelLoad();
280
286
  }
281
287
  };
282
288
  }, [hasSsrPayload, router]);
283
- // Always use RouterProvider directly — matching the server-rendered tree.
284
- // RouterClient is NOT used because it wraps RouterProvider in <Await> which
285
- // introduces a Suspense boundary that doesn't exist in the server HTML,
286
- // causing React hydration mismatch errors.
287
- //
288
- // RouteChunkPreloadGate blocks re-renders until matched lazy chunks finish
289
- // preloading (when preload support is available). During the initial SSR
290
- // hydration render, the gate is skipped to match the server-rendered tree
291
- // and avoid a hydration mismatch. After the post-mount effect runs
292
- // (didTriggerPostHydrationLoadRef becomes true), the gate activates normally.
293
- let app = createElement(Suspense, { fallback: null }, createElement(RouteChunkPreloadGate, {
294
- preloadPromise: routeChunkPreloadPromiseRef.current,
295
- preloadSettledRef: routeChunkPreloadSettledRef,
296
- isHydrating: hasSsrPayload && !didTriggerPostHydrationLoadRef.current,
297
- }, createElement(RouterProvider, { router })));
289
+ // Render RouterProvider directly — matching the server-rendered tree
290
+ // (AppWrapper > RouterProvider). Any extra Suspense boundary here produces
291
+ // a shape mismatch during hydration and React bails to full client-side
292
+ // rendering. The old RouteChunkPreloadGate also suspended re-renders during
293
+ // chunk preload; that behavior is intentionally not replicated here because
294
+ // chunk-preload sequencing is enforced by runPostHydrationLoad before
295
+ // router.load(). A consequence is that any parent-triggered re-render
296
+ // landing in the window between render-phase preload init and
297
+ // runPostHydrationLoad completion now reaches RouterProvider unguarded
298
+ // safe because router.ssr blocks Transitioner-initiated navigation across
299
+ // that window, and the route components themselves throw their own
300
+ // Suspense promises if a chunk is still loading.
301
+ let app = createElement(RouterProvider, { router });
298
302
  if (options.AppWrapper) {
299
303
  const wrapperProps = { ...incomingProps };
300
304
  delete wrapperProps.__tanstackRouterDehydratedState;
@@ -1,3 +1,4 @@
1
+ import 'react-on-rails-rsc/client.browser';
1
2
  import { ReactComponentOrRenderFunction, RenderFunction } from 'react-on-rails/types';
2
3
  /**
3
4
  * Wraps a client component with the necessary RSC context and handling for client-side operations.
@@ -12,6 +12,16 @@ import { jsx as _jsx } from "react/jsx-runtime";
12
12
  * For licensing terms, please see:
13
13
  * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
14
14
  */
15
+ // Side-effect import: keeps `react-on-rails-rsc/client.browser` in the webpack
16
+ // module graph for the client bundle so RSCWebpackPlugin (which scans every
17
+ // parsed module for this exact resource) can detect the client runtime and
18
+ // emit `react-client-manifest.json`. Without this direct import, the plugin
19
+ // relies on a 3-level transitive chain
20
+ // (`wrapServerComponentRenderer/client` → `getReactServerComponent.client`
21
+ // → `react-on-rails-rsc/client.browser`). Any tooling that severs that chain
22
+ // (tree-shaking, transpilers, NormalModuleReplacement, custom externals)
23
+ // silently drops the manifest and breaks RSC hydration on the renderer.
24
+ import 'react-on-rails-rsc/client.browser';
15
25
  import * as React from 'react';
16
26
  import * as ReactDOMClient from 'react-dom/client';
17
27
  import isRenderFunction from 'react-on-rails/isRenderFunction';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-on-rails-pro",
3
- "version": "16.7.0-rc.0",
3
+ "version": "16.7.0-rc.2",
4
4
  "description": "React on Rails Pro package with React Server Components support",
5
5
  "main": "lib/ReactOnRails.full.js",
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  "./ServerComponentFetchError": "./lib/ServerComponentFetchError.js"
49
49
  },
50
50
  "dependencies": {
51
- "react-on-rails": "16.7.0-rc.0"
51
+ "react-on-rails": "16.7.0-rc.2"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "react": ">= 16",