@nestjs-ssr/react 0.3.4 → 0.3.6

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.
@@ -115,8 +115,15 @@ function hasLayout(
115
115
  }
116
116
 
117
117
  /**
118
- * Compose a component with its layout (and nested layouts if any)
119
- * This must match the server-side composition in entry-server.tsx
118
+ * Compose a component with its layout (and nested layouts if any).
119
+ * This must match the server-side composition in entry-server.tsx.
120
+ *
121
+ * The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
122
+ * We iterate in REVERSE order because wrapping happens inside-out:
123
+ * - Start with Page
124
+ * - Wrap with innermost layout first (MethodLayout)
125
+ * - Then wrap with ControllerLayout
126
+ * - Finally wrap with RootLayout (outermost)
120
127
  */
121
128
  function composeWithLayout(
122
129
  ViewComponent: React.ComponentType<any>,
@@ -139,9 +146,11 @@ function composeWithLayout(
139
146
  }
140
147
  }
141
148
 
142
- // Wrap with each layout in the chain
149
+ // Wrap with each layout in REVERSE order (innermost to outermost)
150
+ // This produces the correct nesting: RootLayout > ControllerLayout > Page
143
151
  // Must match server-side wrapping with data-layout and data-outlet attributes
144
- for (const { layout: Layout, props: layoutProps } of layouts) {
152
+ for (let i = layouts.length - 1; i >= 0; i--) {
153
+ const { layout: Layout, props: layoutProps } = layouts[i];
145
154
  const layoutName = Layout.displayName || Layout.name || 'Layout';
146
155
  result = (
147
156
  <div data-layout={layoutName}>
@@ -183,8 +192,18 @@ hydrateRoot(
183
192
  <StrictMode>{wrappedElement}</StrictMode>,
184
193
  );
185
194
 
195
+ // Track if initial hydration is complete to ignore false popstate events
196
+ let hydrationComplete = false;
197
+ requestAnimationFrame(() => {
198
+ hydrationComplete = true;
199
+ });
200
+
186
201
  // Handle browser back/forward navigation
187
202
  window.addEventListener('popstate', async () => {
203
+ // Ignore popstate events that fire before hydration is complete
204
+ // (some browsers fire popstate on initial page load)
205
+ if (!hydrationComplete) return;
206
+
188
207
  // Dynamically import navigate to avoid circular dependency with hydrate-segment
189
208
  const { navigate } = await import('@nestjs-ssr/react/client');
190
209
  // Re-navigate to the current URL (browser already updated location)
@@ -25,6 +25,13 @@ export function getRootLayout(): React.ComponentType<any> | null {
25
25
  * Layouts are passed from the RenderInterceptor based on decorators.
26
26
  * Each layout is wrapped with data-layout and data-outlet attributes
27
27
  * for client-side navigation segment swapping.
28
+ *
29
+ * The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
30
+ * We iterate in REVERSE order because wrapping happens inside-out:
31
+ * - Start with Page
32
+ * - Wrap with innermost layout first (MethodLayout)
33
+ * - Then wrap with ControllerLayout
34
+ * - Finally wrap with RootLayout (outermost)
28
35
  */
29
36
  function composeWithLayouts(
30
37
  ViewComponent: React.ComponentType<any>,
@@ -35,11 +42,12 @@ function composeWithLayouts(
35
42
  // Start with the page component
36
43
  let result = <ViewComponent {...props} />;
37
44
 
38
- // Wrap with each layout in the chain (outermost to innermost in array)
39
- // We iterate normally because layouts are already in correct order from interceptor
45
+ // Wrap with each layout in REVERSE order (innermost to outermost)
46
+ // This produces the correct nesting: RootLayout > ControllerLayout > Page
40
47
  // Pass context to layouts so they can access path, params, etc. for navigation
41
48
  // Each layout gets data-layout attribute and children are wrapped in data-outlet
42
- for (const { layout: Layout, props: layoutProps } of layouts) {
49
+ for (let i = layouts.length - 1; i >= 0; i--) {
50
+ const { layout: Layout, props: layoutProps } = layouts[i];
43
51
  const layoutName = Layout.displayName || Layout.name || 'Layout';
44
52
  result = (
45
53
  <div data-layout={layoutName}>
@@ -80,19 +88,28 @@ export function renderComponent(
80
88
  }
81
89
 
82
90
  /**
83
- * Render just the page component for segment navigation.
84
- * No layout wrappers - the layout already exists on the client.
91
+ * Render a segment for client-side navigation.
92
+ * Includes any layouts below the swap target (e.g., nested layouts).
93
+ * The swap target's outlet will receive this rendered content.
85
94
  */
86
95
  export function renderSegment(
87
96
  ViewComponent: React.ComponentType<any>,
88
97
  data: any,
89
98
  ) {
90
- const { data: pageData, __context: context } = data;
99
+ const { data: pageData, __context: context, __layouts: layouts } = data;
100
+
101
+ // Compose with filtered layouts (layouts below the swap target)
102
+ const composedElement = composeWithLayouts(
103
+ ViewComponent,
104
+ pageData,
105
+ layouts,
106
+ context,
107
+ );
91
108
 
92
- // Render just the page component, no layout wrappers
109
+ // Wrap with PageContextProvider to make context available via hooks
93
110
  const element = (
94
111
  <PageContextProvider context={context}>
95
- <ViewComponent {...pageData} />
112
+ {composedElement}
96
113
  </PageContextProvider>
97
114
  );
98
115
 
@@ -1,4 +1,4 @@
1
- import { H as HeadData, R as RenderContext } from './render-response.interface-CxbuKGnV.mjs';
1
+ import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.mjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
4
 
@@ -85,6 +85,7 @@ declare function updatePageContext(context: RenderContext): void;
85
85
  *
86
86
  * @param isSegment - If true, this is a segment provider (for hydrated segments)
87
87
  * and won't register its setter to avoid overwriting the root provider's.
88
+ * However, it will still receive broadcasts when context updates.
88
89
  */
89
90
  declare function PageContextProvider({ context: initialContext, children, isSegment, }: {
90
91
  context: RenderContext;
@@ -1,4 +1,4 @@
1
- import { H as HeadData, R as RenderContext } from './render-response.interface-CxbuKGnV.js';
1
+ import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
4
 
@@ -85,6 +85,7 @@ declare function updatePageContext(context: RenderContext): void;
85
85
  *
86
86
  * @param isSegment - If true, this is a segment provider (for hydrated segments)
87
87
  * and won't register its setter to avoid overwriting the root provider's.
88
+ * However, it will still receive broadcasts when context updates.
88
89
  */
89
90
  declare function PageContextProvider({ context: initialContext, children, isSegment, }: {
90
91
  context: RenderContext;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-ssr/react",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "React SSR for NestJS that respects Clean Architecture. Proper DI, SOLID principles, clear separation of concerns.",
5
5
  "keywords": [
6
6
  "nestjs",
@@ -112,6 +112,8 @@
112
112
  "@nestjs/common": "^11.0.0",
113
113
  "@nestjs/core": "^11.0.0",
114
114
  "@nestjs/platform-express": "^11.0.0",
115
+ "@nestjs/platform-fastify": "^11.0.0",
116
+ "@fastify/static": "^8.0.0 || ^7.0.0",
115
117
  "http-proxy-middleware": "^3.0.0 || ^2.0.0",
116
118
  "react": "^19.0.0",
117
119
  "react-dom": "^19.0.0",
@@ -119,6 +121,15 @@
119
121
  "vite": "^7.0.0 || ^6.0.0"
120
122
  },
121
123
  "peerDependenciesMeta": {
124
+ "@nestjs/platform-express": {
125
+ "optional": true
126
+ },
127
+ "@nestjs/platform-fastify": {
128
+ "optional": true
129
+ },
130
+ "@fastify/static": {
131
+ "optional": true
132
+ },
122
133
  "http-proxy-middleware": {
123
134
  "optional": true
124
135
  }
@@ -115,8 +115,15 @@ function hasLayout(
115
115
  }
116
116
 
117
117
  /**
118
- * Compose a component with its layout (and nested layouts if any)
119
- * This must match the server-side composition in entry-server.tsx
118
+ * Compose a component with its layout (and nested layouts if any).
119
+ * This must match the server-side composition in entry-server.tsx.
120
+ *
121
+ * The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
122
+ * We iterate in REVERSE order because wrapping happens inside-out:
123
+ * - Start with Page
124
+ * - Wrap with innermost layout first (MethodLayout)
125
+ * - Then wrap with ControllerLayout
126
+ * - Finally wrap with RootLayout (outermost)
120
127
  */
121
128
  function composeWithLayout(
122
129
  ViewComponent: React.ComponentType<any>,
@@ -139,9 +146,11 @@ function composeWithLayout(
139
146
  }
140
147
  }
141
148
 
142
- // Wrap with each layout in the chain
149
+ // Wrap with each layout in REVERSE order (innermost to outermost)
150
+ // This produces the correct nesting: RootLayout > ControllerLayout > Page
143
151
  // Must match server-side wrapping with data-layout and data-outlet attributes
144
- for (const { layout: Layout, props: layoutProps } of layouts) {
152
+ for (let i = layouts.length - 1; i >= 0; i--) {
153
+ const { layout: Layout, props: layoutProps } = layouts[i];
145
154
  const layoutName = Layout.displayName || Layout.name || 'Layout';
146
155
  result = (
147
156
  <div data-layout={layoutName}>
@@ -183,8 +192,18 @@ hydrateRoot(
183
192
  <StrictMode>{wrappedElement}</StrictMode>,
184
193
  );
185
194
 
195
+ // Track if initial hydration is complete to ignore false popstate events
196
+ let hydrationComplete = false;
197
+ requestAnimationFrame(() => {
198
+ hydrationComplete = true;
199
+ });
200
+
186
201
  // Handle browser back/forward navigation
187
202
  window.addEventListener('popstate', async () => {
203
+ // Ignore popstate events that fire before hydration is complete
204
+ // (some browsers fire popstate on initial page load)
205
+ if (!hydrationComplete) return;
206
+
188
207
  // Dynamically import navigate to avoid circular dependency with hydrate-segment
189
208
  const { navigate } = await import('@nestjs-ssr/react/client');
190
209
  // Re-navigate to the current URL (browser already updated location)
@@ -25,6 +25,13 @@ export function getRootLayout(): React.ComponentType<any> | null {
25
25
  * Layouts are passed from the RenderInterceptor based on decorators.
26
26
  * Each layout is wrapped with data-layout and data-outlet attributes
27
27
  * for client-side navigation segment swapping.
28
+ *
29
+ * The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
30
+ * We iterate in REVERSE order because wrapping happens inside-out:
31
+ * - Start with Page
32
+ * - Wrap with innermost layout first (MethodLayout)
33
+ * - Then wrap with ControllerLayout
34
+ * - Finally wrap with RootLayout (outermost)
28
35
  */
29
36
  function composeWithLayouts(
30
37
  ViewComponent: React.ComponentType<any>,
@@ -35,11 +42,12 @@ function composeWithLayouts(
35
42
  // Start with the page component
36
43
  let result = <ViewComponent {...props} />;
37
44
 
38
- // Wrap with each layout in the chain (outermost to innermost in array)
39
- // We iterate normally because layouts are already in correct order from interceptor
45
+ // Wrap with each layout in REVERSE order (innermost to outermost)
46
+ // This produces the correct nesting: RootLayout > ControllerLayout > Page
40
47
  // Pass context to layouts so they can access path, params, etc. for navigation
41
48
  // Each layout gets data-layout attribute and children are wrapped in data-outlet
42
- for (const { layout: Layout, props: layoutProps } of layouts) {
49
+ for (let i = layouts.length - 1; i >= 0; i--) {
50
+ const { layout: Layout, props: layoutProps } = layouts[i];
43
51
  const layoutName = Layout.displayName || Layout.name || 'Layout';
44
52
  result = (
45
53
  <div data-layout={layoutName}>
@@ -80,19 +88,28 @@ export function renderComponent(
80
88
  }
81
89
 
82
90
  /**
83
- * Render just the page component for segment navigation.
84
- * No layout wrappers - the layout already exists on the client.
91
+ * Render a segment for client-side navigation.
92
+ * Includes any layouts below the swap target (e.g., nested layouts).
93
+ * The swap target's outlet will receive this rendered content.
85
94
  */
86
95
  export function renderSegment(
87
96
  ViewComponent: React.ComponentType<any>,
88
97
  data: any,
89
98
  ) {
90
- const { data: pageData, __context: context } = data;
99
+ const { data: pageData, __context: context, __layouts: layouts } = data;
100
+
101
+ // Compose with filtered layouts (layouts below the swap target)
102
+ const composedElement = composeWithLayouts(
103
+ ViewComponent,
104
+ pageData,
105
+ layouts,
106
+ context,
107
+ );
91
108
 
92
- // Render just the page component, no layout wrappers
109
+ // Wrap with PageContextProvider to make context available via hooks
93
110
  const element = (
94
111
  <PageContextProvider context={context}>
95
- <ViewComponent {...pageData} />
112
+ {composedElement}
96
113
  </PageContextProvider>
97
114
  );
98
115