@tramvai/module-render 2.39.3 → 2.44.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
@@ -143,13 +143,13 @@ To speed up data loading, we've added a preloading system for resources and asyn
143
143
  - Next we add all the CSS to the **preload** tag and add onload event on them. We need to load the blocking resources as quickly as possible.
144
144
  - When loading any CSS file, onload event will be fired (only once time) and add all **preload** tags to the necessary JS files
145
145
 
146
- ### Basic layout
146
+ ### Root layout
147
147
 
148
- The `RenderModule` has a default basic layout that supports different ways of extending and adding functionality
148
+ The `RenderModule` has a default root layout that supports different ways of extending and adding functionality
149
149
 
150
150
  [Read more about layout on the library page](references/libs/tinkoff-layout.md)
151
151
 
152
- #### Adding a basic header and footer
152
+ #### Adding a default header and footer
153
153
 
154
154
  The module allows you to add header and footer components, which will be rendered by default for all pages
155
155
 
@@ -191,7 +191,7 @@ createBundle({
191
191
 
192
192
  #### Adding components and wrappers
193
193
 
194
- You can add custom components and wrappers for layout via the token `LAYOUT_OPTIONS`
194
+ You can add custom components and wrappers for layout via the token `LAYOUT_OPTIONS`, this wrappers will be applied on every application page:
195
195
 
196
196
  ```tsx
197
197
  import { provide } from '@tramvai/core';
@@ -226,7 +226,13 @@ export class MyLayoutModule {}
226
226
 
227
227
  More details about the `components` and `wrappers` options can be found in [@tinkoff/layout-factory](references/libs/tinkoff-layout.md)
228
228
 
229
- #### Replacing the basic layout
229
+ #### Replacing the root layout
230
+
231
+ :::warning
232
+
233
+ Not recommended, because a lot of dependant features can be broken!
234
+
235
+ :::
230
236
 
231
237
  If the basic layout doesn't work for you, you can replace it with any other React component.
232
238
  In doing so, you need to implement all the wrappers yourself and plug in global components if you need them.
@@ -268,6 +274,64 @@ createBundle({
268
274
  });
269
275
  ```
270
276
 
277
+ ### Nested layout
278
+
279
+ For every page, nested layout can be applied. It is useful when you need to wrap group of pages in the same block, or add the same actions.
280
+
281
+ :::note
282
+
283
+ For now, only one level of layout nesting supported, and simplified component structure will look like this:
284
+
285
+ ```tsx
286
+ <RootLayout>
287
+ <NestedLayout>
288
+ <Page />
289
+ </NestedLayout>
290
+ </RootLayout>
291
+ ```
292
+
293
+ :::
294
+
295
+ Nested layout it is a simple React component with `children` property, and static properties `actions` and `reducers` are supported:
296
+
297
+ ```tsx
298
+ import type { NestedLayoutComponent } from '@tramvai/react';
299
+
300
+ const Layout: NestedLayoutComponent = ({ children }) => {
301
+ return <div>{children}</div>;
302
+ };
303
+
304
+ Layout.actions = [actionFoo, actionBar];
305
+
306
+ Layout.reducers = [StoreBaz];
307
+ ```
308
+
309
+ Actions will be registered as current page component actions and reducers will be registered in global store.
310
+
311
+ #### Adding a nested layout
312
+
313
+ You can add a `nestedLayoutComponent` property to route `config` property and register component in `bundle`.
314
+ This layout will be rendered when you go to the corresponding route.
315
+
316
+ ```tsx
317
+ createBundle({
318
+ name: 'common-bundle',
319
+ components: {
320
+ myNestedLayout: NestedLayout,
321
+ },
322
+ });
323
+
324
+ const route = {
325
+ name: 'main',
326
+ path: '/',
327
+ config: {
328
+ nestedLayoutComponent: 'myNestedLayout',
329
+ },
330
+ };
331
+ ```
332
+
333
+ Also, this feature available for [File-System Pages and Routes](features/routing/file-system-pages.md)
334
+
271
335
  ## How to
272
336
 
273
337
  ### How to add assets loading to a page
package/lib/browser.js CHANGED
@@ -3,36 +3,64 @@ import { Module, provide, commandLineListTokens, DI_TOKEN } from '@tramvai/core'
3
3
  import { COMBINE_REDUCERS, STORE_TOKEN, LOGGER_TOKEN, CONTEXT_TOKEN } from '@tramvai/tokens-common';
4
4
  import { DEFAULT_LAYOUT_COMPONENT, LAYOUT_OPTIONS, DEFAULT_FOOTER_COMPONENT, DEFAULT_HEADER_COMPONENT, TRAMVAI_RENDER_MODE, RESOURCES_REGISTRY, CUSTOM_RENDER, EXTEND_RENDER, RENDERER_CALLBACK, USE_REACT_STRICT_MODE, RENDER_MODE } from '@tramvai/tokens-render';
5
5
  export * from '@tramvai/tokens-render';
6
- import { ROUTER_TOKEN, PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
6
+ import { PAGE_SERVICE_TOKEN, ROUTER_TOKEN } from '@tramvai/tokens-router';
7
7
  import each from '@tinkoff/utils/array/each';
8
- import { PureComponent, useMemo, createElement, StrictMode } from 'react';
8
+ import { useMemo, createElement, StrictMode } from 'react';
9
9
  import { jsx } from 'react/jsx-runtime';
10
10
  import { createEvent, createReducer, useStore, Provider } from '@tramvai/state';
11
11
  import { useDi, ERROR_BOUNDARY_TOKEN, ERROR_BOUNDARY_FALLBACK_COMPONENT_TOKEN, UniversalErrorBoundary, DIContext } from '@tramvai/react';
12
- import { useRoute, useUrl } from '@tramvai/module-router';
12
+ import { usePageService, useUrl } from '@tramvai/module-router';
13
13
  import { useIsomorphicLayoutEffect } from '@tinkoff/react-hooks';
14
14
  import { composeLayoutOptions, createLayout } from '@tinkoff/layout-factory';
15
15
 
16
- class RootComponent extends PureComponent {
17
- render() {
18
- const { LayoutComponent, PageComponent, HeaderComponent, FooterComponent } = this.props;
19
- return (jsx(LayoutComponent, { Header: HeaderComponent, Footer: FooterComponent, children: jsx(PageComponent, {}) }));
20
- }
21
- }
22
- const Root = ({ pageService }) => {
23
- const { config } = useRoute();
24
- const { pageComponent } = config;
16
+ /**
17
+ * Result component structure:
18
+ *
19
+ * <Root>
20
+ * <RootComponent>
21
+ * <LayoutComponent>
22
+ * <NestedLayoutComponent>
23
+ * <PageComponent />
24
+ * </NestedLayoutComponent>
25
+ * </LayoutComponent>
26
+ * </RootComponent>
27
+ * </Root>
28
+ *
29
+ * All components separated for a few reasons:
30
+ * - Page subtree can be rendered independently when Layout and Nested Layout the same
31
+ * - Nested Layout can be rerendered only on its changes
32
+ * - Layout can be rendered only on its changes
33
+ */
34
+ const LayoutRenderComponent = ({ children }) => {
35
+ const pageService = usePageService();
36
+ const LayoutComponent = pageService.resolveComponentFromConfig('layout');
37
+ const HeaderComponent = pageService.resolveComponentFromConfig('header');
38
+ const FooterComponent = pageService.resolveComponentFromConfig('footer');
39
+ const layout = useMemo(() => (jsx(LayoutComponent, { Header: HeaderComponent, Footer: FooterComponent, children: children })), [LayoutComponent, HeaderComponent, FooterComponent, children]);
40
+ return layout;
41
+ };
42
+ const NestedLayoutRenderComponent = ({ children }) => {
43
+ const pageService = usePageService();
44
+ const NestedLayoutComponent = pageService.resolveComponentFromConfig('nestedLayout');
45
+ const nestedLayout = useMemo(() => jsx(NestedLayoutComponent, { children: children }), [NestedLayoutComponent, children]);
46
+ return nestedLayout;
47
+ };
48
+ const PageRenderComponent = () => {
49
+ const pageService = usePageService();
50
+ const { pageComponent } = pageService.getConfig();
25
51
  let PageComponent = pageService.getComponent(pageComponent);
26
52
  if (!PageComponent) {
27
53
  PageComponent = () => {
28
54
  throw new Error(`Page component '${pageComponent}' not found`);
29
55
  };
30
56
  }
31
- // Get components for current page, otherwise use a defaults
32
- const LayoutComponent = pageService.resolveComponentFromConfig('layout');
33
- const HeaderComponent = pageService.resolveComponentFromConfig('header');
34
- const FooterComponent = pageService.resolveComponentFromConfig('footer');
35
- return (jsx(RootComponent, { HeaderComponent: HeaderComponent, FooterComponent: FooterComponent, LayoutComponent: LayoutComponent, PageComponent: PageComponent }));
57
+ const page = useMemo(() => jsx(PageComponent, {}), [PageComponent]);
58
+ return page;
59
+ };
60
+ const Root = () => {
61
+ const pageRenderComponent = useMemo(() => jsx(PageRenderComponent, {}), []);
62
+ const nestedLayoutRenderComponent = useMemo(() => jsx(NestedLayoutRenderComponent, { children: pageRenderComponent }), [pageRenderComponent]);
63
+ return jsx(LayoutRenderComponent, { children: nestedLayoutRenderComponent });
36
64
  };
37
65
 
38
66
  function serializeError(error) {
@@ -52,7 +80,8 @@ const initialState = null;
52
80
  const PageErrorStore = createReducer('pageError', initialState).on(setPageErrorEvent, (state, error) => error && serializeError(error));
53
81
 
54
82
  const PageErrorBoundary = (props) => {
55
- const { children, pageService } = props;
83
+ const { children } = props;
84
+ const pageService = useDi(PAGE_SERVICE_TOKEN);
56
85
  const url = useUrl();
57
86
  const serializedError = useStore(PageErrorStore);
58
87
  const error = useMemo(() => {
@@ -64,9 +93,9 @@ const PageErrorBoundary = (props) => {
64
93
  return (jsx(UniversalErrorBoundary, { url: url, error: error, errorHandlers: errorHandlers, fallback: fallback, fallbackFromDi: fallbackFromDi, children: children }));
65
94
  };
66
95
 
67
- function renderReact({ pageService, di }, context) {
96
+ function renderReact({ di }, context) {
68
97
  const serverState = typeof window !== 'undefined' ? context.getState() : undefined;
69
- return (jsx(Provider, { context: context, serverState: serverState, children: jsx(DIContext.Provider, { value: di, children: jsx(PageErrorBoundary, { pageService: pageService, children: jsx(Root, { pageService: pageService }) }) }) }));
98
+ return (jsx(Provider, { context: context, serverState: serverState, children: jsx(DIContext.Provider, { value: di, children: jsx(PageErrorBoundary, { children: jsx(Root, {}) }) }) }));
70
99
  }
71
100
 
72
101
  let hydrateRoot;
@@ -98,10 +127,10 @@ const renderer = ({ element, container, callback, log }) => {
98
127
  return hydrate(element, container, callback);
99
128
  };
100
129
 
101
- function rendering({ pageService, logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, }) {
130
+ function rendering({ logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, }) {
102
131
  const log = logger('module-render');
103
132
  return new Promise((resolve, reject) => {
104
- let renderResult = renderReact({ pageService, di }, consumerContext);
133
+ let renderResult = renderReact({ di }, consumerContext);
105
134
  if (extendRender) {
106
135
  each((render) => {
107
136
  renderResult = render(renderResult);
@@ -140,6 +169,7 @@ function rendering({ pageService, logger, consumerContext, customRender, extendR
140
169
  });
141
170
  }
142
171
 
172
+ const RenderChildrenComponent = ({ children }) => children;
143
173
  let LayoutModule = class LayoutModule {
144
174
  };
145
175
  LayoutModule = __decorate([
@@ -158,7 +188,10 @@ LayoutModule = __decorate([
158
188
  {
159
189
  provide: 'componentDefaultList',
160
190
  multi: true,
161
- useFactory: (components) => components,
191
+ useFactory: (components) => ({
192
+ ...components,
193
+ nestedLayoutDefault: RenderChildrenComponent,
194
+ }),
162
195
  deps: {
163
196
  layoutDefault: DEFAULT_LAYOUT_COMPONENT,
164
197
  footerDefault: { token: DEFAULT_FOOTER_COMPONENT, optional: true },
@@ -247,7 +280,6 @@ RenderModule = RenderModule_1 = __decorate([
247
280
  };
248
281
  },
249
282
  deps: {
250
- pageService: PAGE_SERVICE_TOKEN,
251
283
  logger: LOGGER_TOKEN,
252
284
  customRender: { token: CUSTOM_RENDER, optional: true },
253
285
  extendRender: { token: EXTEND_RENDER, optional: true },
@@ -1,8 +1,6 @@
1
1
  import type { EXTEND_RENDER, RENDERER_CALLBACK, USE_REACT_STRICT_MODE } from '@tramvai/tokens-render';
2
- import type { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
3
2
  import type { ExtractDependencyType } from '@tinkoff/dippy';
4
- export declare function rendering({ pageService, logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, }: {
5
- pageService: ExtractDependencyType<typeof PAGE_SERVICE_TOKEN>;
3
+ export declare function rendering({ logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, }: {
6
4
  logger: any;
7
5
  consumerContext: any;
8
6
  extendRender?: ExtractDependencyType<typeof EXTEND_RENDER>;
@@ -1,4 +1,4 @@
1
- declare type Renderer = (params: {
1
+ type Renderer = (params: {
2
2
  element: any;
3
3
  container: Element;
4
4
  callback: () => void;
@@ -1,4 +1,3 @@
1
- export declare function renderReact({ pageService, di }: {
2
- pageService: any;
1
+ export declare function renderReact({ di }: {
3
2
  di: any;
4
3
  }, context: any): JSX.Element;
@@ -1,5 +1,2 @@
1
1
  import type { PropsWithChildren } from 'react';
2
- import type { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
3
- export declare const PageErrorBoundary: (props: PropsWithChildren<{
4
- pageService: typeof PAGE_SERVICE_TOKEN;
5
- }>) => JSX.Element;
2
+ export declare const PageErrorBoundary: (props: PropsWithChildren) => JSX.Element;
@@ -1,4 +1 @@
1
- import type { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
2
- export declare const Root: ({ pageService }: {
3
- pageService: typeof PAGE_SERVICE_TOKEN;
4
- }) => JSX.Element;
1
+ export declare const Root: () => JSX.Element;
@@ -6,7 +6,7 @@ import type { ResourcesInlinerType } from './resourcesInliner';
6
6
  * в итоговую html-страницу в виде ссылки на файл или заинлайнеными полностью
7
7
  */
8
8
  export declare const RESOURCE_INLINER: import("@tinkoff/dippy").BaseTokenInterface<ResourcesInlinerType>;
9
- export declare type ResourcesRegistryCache = {
9
+ export type ResourcesRegistryCache = {
10
10
  filesCache: Cache;
11
11
  sizeCache: Cache;
12
12
  disabledUrlsCache: Cache;
@@ -1,6 +1,6 @@
1
1
  import type { RESOURCES_REGISTRY } from '@tramvai/tokens-render';
2
- declare type ResourceRegistry = typeof RESOURCES_REGISTRY;
3
- declare type PageResource = ReturnType<ResourceRegistry['getPageResources']>[0];
2
+ type ResourceRegistry = typeof RESOURCES_REGISTRY;
3
+ type PageResource = ReturnType<ResourceRegistry['getPageResources']>[0];
4
4
  export declare class ResourcesRegistry implements ResourceRegistry {
5
5
  private resources;
6
6
  private resourceInliner;
@@ -2,18 +2,15 @@ import type { ExtractDependencyType } from '@tinkoff/dippy';
2
2
  import type { DI_TOKEN } from '@tramvai/core';
3
3
  import type { CONTEXT_TOKEN, LOGGER_TOKEN } from '@tramvai/module-common';
4
4
  import type { EXTEND_RENDER, CUSTOM_RENDER, REACT_SERVER_RENDER_MODE } from '@tramvai/tokens-render';
5
- import type { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
6
5
  import type { ChunkExtractor } from '@loadable/server';
7
6
  export declare class ReactRenderServer {
8
7
  customRender: typeof CUSTOM_RENDER;
9
8
  extendRender: ExtractDependencyType<typeof EXTEND_RENDER>;
10
9
  context: typeof CONTEXT_TOKEN;
11
- pageService: typeof PAGE_SERVICE_TOKEN;
12
10
  di: typeof DI_TOKEN;
13
11
  log: ReturnType<typeof LOGGER_TOKEN>;
14
12
  renderMode: typeof REACT_SERVER_RENDER_MODE;
15
- constructor({ pageService, context, customRender, extendRender, di, renderMode, logger }: {
16
- pageService: any;
13
+ constructor({ context, customRender, extendRender, di, renderMode, logger }: {
17
14
  context: any;
18
15
  customRender: any;
19
16
  extendRender: any;
@@ -1,4 +1,4 @@
1
- export declare type WebpackStats = {
1
+ export type WebpackStats = {
2
2
  assetsByChunkName: Record<string, string[]>;
3
3
  namedChunkGroups?: Record<string, {
4
4
  name: string;
package/lib/server.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from 'tslib';
2
- import { PureComponent, useMemo, createElement } from 'react';
2
+ import { useMemo, createElement } from 'react';
3
3
  import { renderToString } from 'react-dom/server';
4
4
  import { Module, provide, commandLineListTokens, DI_TOKEN } from '@tramvai/core';
5
5
  import { COMBINE_REDUCERS, CREATE_CACHE_TOKEN, LOGGER_TOKEN, REQUEST_MANAGER_TOKEN, RESPONSE_MANAGER_TOKEN, CONTEXT_TOKEN } from '@tramvai/tokens-common';
@@ -32,7 +32,7 @@ import { o as onload } from './server_inline.inline.es.js';
32
32
  import { Writable } from 'stream';
33
33
  import { jsx } from 'react/jsx-runtime';
34
34
  import { createEvent, createReducer, useStore, Provider } from '@tramvai/state';
35
- import { useRoute, useUrl } from '@tramvai/module-router';
35
+ import { usePageService, useUrl } from '@tramvai/module-router';
36
36
  import { composeLayoutOptions, createLayout } from '@tinkoff/layout-factory';
37
37
 
38
38
  const thirtySeconds = 1000 * 30;
@@ -647,26 +647,54 @@ const htmlPageSchemaFactory = ({ htmlAttrs, }) => {
647
647
  ];
648
648
  };
649
649
 
650
- class RootComponent extends PureComponent {
651
- render() {
652
- const { LayoutComponent, PageComponent, HeaderComponent, FooterComponent } = this.props;
653
- return (jsx(LayoutComponent, { Header: HeaderComponent, Footer: FooterComponent, children: jsx(PageComponent, {}) }));
654
- }
655
- }
656
- const Root = ({ pageService }) => {
657
- const { config } = useRoute();
658
- const { pageComponent } = config;
650
+ /**
651
+ * Result component structure:
652
+ *
653
+ * <Root>
654
+ * <RootComponent>
655
+ * <LayoutComponent>
656
+ * <NestedLayoutComponent>
657
+ * <PageComponent />
658
+ * </NestedLayoutComponent>
659
+ * </LayoutComponent>
660
+ * </RootComponent>
661
+ * </Root>
662
+ *
663
+ * All components separated for a few reasons:
664
+ * - Page subtree can be rendered independently when Layout and Nested Layout the same
665
+ * - Nested Layout can be rerendered only on its changes
666
+ * - Layout can be rendered only on its changes
667
+ */
668
+ const LayoutRenderComponent = ({ children }) => {
669
+ const pageService = usePageService();
670
+ const LayoutComponent = pageService.resolveComponentFromConfig('layout');
671
+ const HeaderComponent = pageService.resolveComponentFromConfig('header');
672
+ const FooterComponent = pageService.resolveComponentFromConfig('footer');
673
+ const layout = useMemo(() => (jsx(LayoutComponent, { Header: HeaderComponent, Footer: FooterComponent, children: children })), [LayoutComponent, HeaderComponent, FooterComponent, children]);
674
+ return layout;
675
+ };
676
+ const NestedLayoutRenderComponent = ({ children }) => {
677
+ const pageService = usePageService();
678
+ const NestedLayoutComponent = pageService.resolveComponentFromConfig('nestedLayout');
679
+ const nestedLayout = useMemo(() => jsx(NestedLayoutComponent, { children: children }), [NestedLayoutComponent, children]);
680
+ return nestedLayout;
681
+ };
682
+ const PageRenderComponent = () => {
683
+ const pageService = usePageService();
684
+ const { pageComponent } = pageService.getConfig();
659
685
  let PageComponent = pageService.getComponent(pageComponent);
660
686
  if (!PageComponent) {
661
687
  PageComponent = () => {
662
688
  throw new Error(`Page component '${pageComponent}' not found`);
663
689
  };
664
690
  }
665
- // Get components for current page, otherwise use a defaults
666
- const LayoutComponent = pageService.resolveComponentFromConfig('layout');
667
- const HeaderComponent = pageService.resolveComponentFromConfig('header');
668
- const FooterComponent = pageService.resolveComponentFromConfig('footer');
669
- return (jsx(RootComponent, { HeaderComponent: HeaderComponent, FooterComponent: FooterComponent, LayoutComponent: LayoutComponent, PageComponent: PageComponent }));
691
+ const page = useMemo(() => jsx(PageComponent, {}), [PageComponent]);
692
+ return page;
693
+ };
694
+ const Root = () => {
695
+ const pageRenderComponent = useMemo(() => jsx(PageRenderComponent, {}), []);
696
+ const nestedLayoutRenderComponent = useMemo(() => jsx(NestedLayoutRenderComponent, { children: pageRenderComponent }), [pageRenderComponent]);
697
+ return jsx(LayoutRenderComponent, { children: nestedLayoutRenderComponent });
670
698
  };
671
699
 
672
700
  function serializeError(error) {
@@ -686,7 +714,8 @@ const initialState = null;
686
714
  const PageErrorStore = createReducer('pageError', initialState).on(setPageErrorEvent, (state, error) => error && serializeError(error));
687
715
 
688
716
  const PageErrorBoundary = (props) => {
689
- const { children, pageService } = props;
717
+ const { children } = props;
718
+ const pageService = useDi(PAGE_SERVICE_TOKEN);
690
719
  const url = useUrl();
691
720
  const serializedError = useStore(PageErrorStore);
692
721
  const error = useMemo(() => {
@@ -698,9 +727,9 @@ const PageErrorBoundary = (props) => {
698
727
  return (jsx(UniversalErrorBoundary, { url: url, error: error, errorHandlers: errorHandlers, fallback: fallback, fallbackFromDi: fallbackFromDi, children: children }));
699
728
  };
700
729
 
701
- function renderReact({ pageService, di }, context) {
730
+ function renderReact({ di }, context) {
702
731
  const serverState = typeof window !== 'undefined' ? context.getState() : undefined;
703
- return (jsx(Provider, { context: context, serverState: serverState, children: jsx(DIContext.Provider, { value: di, children: jsx(PageErrorBoundary, { pageService: pageService, children: jsx(Root, { pageService: pageService }) }) }) }));
732
+ return (jsx(Provider, { context: context, serverState: serverState, children: jsx(DIContext.Provider, { value: di, children: jsx(PageErrorBoundary, { children: jsx(Root, {}) }) }) }));
704
733
  }
705
734
 
706
735
  const RENDER_TIMEOUT = 500;
@@ -724,8 +753,7 @@ class HtmlWritable extends Writable {
724
753
  }
725
754
  class ReactRenderServer {
726
755
  // eslint-disable-next-line sort-class-members/sort-class-members
727
- constructor({ pageService, context, customRender, extendRender, di, renderMode, logger }) {
728
- this.pageService = pageService;
756
+ constructor({ context, customRender, extendRender, di, renderMode, logger }) {
729
757
  this.context = context;
730
758
  this.customRender = customRender;
731
759
  this.extendRender = extendRender;
@@ -735,7 +763,7 @@ class ReactRenderServer {
735
763
  }
736
764
  render(extractor) {
737
765
  var _a;
738
- let renderResult = renderReact({ pageService: this.pageService, di: this.di }, this.context);
766
+ let renderResult = renderReact({ di: this.di }, this.context);
739
767
  each((render) => {
740
768
  renderResult = render(renderResult);
741
769
  }, (_a = this.extendRender) !== null && _a !== void 0 ? _a : []);
@@ -787,6 +815,7 @@ class ReactRenderServer {
787
815
  }
788
816
  }
789
817
 
818
+ const RenderChildrenComponent = ({ children }) => children;
790
819
  let LayoutModule = class LayoutModule {
791
820
  };
792
821
  LayoutModule = __decorate([
@@ -805,7 +834,10 @@ LayoutModule = __decorate([
805
834
  {
806
835
  provide: 'componentDefaultList',
807
836
  multi: true,
808
- useFactory: (components) => components,
837
+ useFactory: (components) => ({
838
+ ...components,
839
+ nestedLayoutDefault: RenderChildrenComponent,
840
+ }),
809
841
  deps: {
810
842
  layoutDefault: DEFAULT_LAYOUT_COMPONENT,
811
843
  footerDefault: { token: DEFAULT_FOOTER_COMPONENT, optional: true },
@@ -965,7 +997,6 @@ RenderModule = RenderModule_1 = __decorate([
965
997
  useClass: ReactRenderServer,
966
998
  deps: {
967
999
  context: CONTEXT_TOKEN,
968
- pageService: PAGE_SERVICE_TOKEN,
969
1000
  customRender: { token: CUSTOM_RENDER, optional: true },
970
1001
  extendRender: { token: EXTEND_RENDER, optional: true },
971
1002
  di: DI_TOKEN,
package/lib/server.js CHANGED
@@ -683,26 +683,54 @@ const htmlPageSchemaFactory = ({ htmlAttrs, }) => {
683
683
  ];
684
684
  };
685
685
 
686
- class RootComponent extends react.PureComponent {
687
- render() {
688
- const { LayoutComponent, PageComponent, HeaderComponent, FooterComponent } = this.props;
689
- return (jsxRuntime.jsx(LayoutComponent, { Header: HeaderComponent, Footer: FooterComponent, children: jsxRuntime.jsx(PageComponent, {}) }));
690
- }
691
- }
692
- const Root = ({ pageService }) => {
693
- const { config } = moduleRouter.useRoute();
694
- const { pageComponent } = config;
686
+ /**
687
+ * Result component structure:
688
+ *
689
+ * <Root>
690
+ * <RootComponent>
691
+ * <LayoutComponent>
692
+ * <NestedLayoutComponent>
693
+ * <PageComponent />
694
+ * </NestedLayoutComponent>
695
+ * </LayoutComponent>
696
+ * </RootComponent>
697
+ * </Root>
698
+ *
699
+ * All components separated for a few reasons:
700
+ * - Page subtree can be rendered independently when Layout and Nested Layout the same
701
+ * - Nested Layout can be rerendered only on its changes
702
+ * - Layout can be rendered only on its changes
703
+ */
704
+ const LayoutRenderComponent = ({ children }) => {
705
+ const pageService = moduleRouter.usePageService();
706
+ const LayoutComponent = pageService.resolveComponentFromConfig('layout');
707
+ const HeaderComponent = pageService.resolveComponentFromConfig('header');
708
+ const FooterComponent = pageService.resolveComponentFromConfig('footer');
709
+ const layout = react.useMemo(() => (jsxRuntime.jsx(LayoutComponent, { Header: HeaderComponent, Footer: FooterComponent, children: children })), [LayoutComponent, HeaderComponent, FooterComponent, children]);
710
+ return layout;
711
+ };
712
+ const NestedLayoutRenderComponent = ({ children }) => {
713
+ const pageService = moduleRouter.usePageService();
714
+ const NestedLayoutComponent = pageService.resolveComponentFromConfig('nestedLayout');
715
+ const nestedLayout = react.useMemo(() => jsxRuntime.jsx(NestedLayoutComponent, { children: children }), [NestedLayoutComponent, children]);
716
+ return nestedLayout;
717
+ };
718
+ const PageRenderComponent = () => {
719
+ const pageService = moduleRouter.usePageService();
720
+ const { pageComponent } = pageService.getConfig();
695
721
  let PageComponent = pageService.getComponent(pageComponent);
696
722
  if (!PageComponent) {
697
723
  PageComponent = () => {
698
724
  throw new Error(`Page component '${pageComponent}' not found`);
699
725
  };
700
726
  }
701
- // Get components for current page, otherwise use a defaults
702
- const LayoutComponent = pageService.resolveComponentFromConfig('layout');
703
- const HeaderComponent = pageService.resolveComponentFromConfig('header');
704
- const FooterComponent = pageService.resolveComponentFromConfig('footer');
705
- return (jsxRuntime.jsx(RootComponent, { HeaderComponent: HeaderComponent, FooterComponent: FooterComponent, LayoutComponent: LayoutComponent, PageComponent: PageComponent }));
727
+ const page = react.useMemo(() => jsxRuntime.jsx(PageComponent, {}), [PageComponent]);
728
+ return page;
729
+ };
730
+ const Root = () => {
731
+ const pageRenderComponent = react.useMemo(() => jsxRuntime.jsx(PageRenderComponent, {}), []);
732
+ const nestedLayoutRenderComponent = react.useMemo(() => jsxRuntime.jsx(NestedLayoutRenderComponent, { children: pageRenderComponent }), [pageRenderComponent]);
733
+ return jsxRuntime.jsx(LayoutRenderComponent, { children: nestedLayoutRenderComponent });
706
734
  };
707
735
 
708
736
  function serializeError(error) {
@@ -722,7 +750,8 @@ const initialState = null;
722
750
  const PageErrorStore = state.createReducer('pageError', initialState).on(setPageErrorEvent, (state, error) => error && serializeError(error));
723
751
 
724
752
  const PageErrorBoundary = (props) => {
725
- const { children, pageService } = props;
753
+ const { children } = props;
754
+ const pageService = react$1.useDi(tokensRouter.PAGE_SERVICE_TOKEN);
726
755
  const url = moduleRouter.useUrl();
727
756
  const serializedError = state.useStore(PageErrorStore);
728
757
  const error = react.useMemo(() => {
@@ -734,9 +763,9 @@ const PageErrorBoundary = (props) => {
734
763
  return (jsxRuntime.jsx(react$1.UniversalErrorBoundary, { url: url, error: error, errorHandlers: errorHandlers, fallback: fallback, fallbackFromDi: fallbackFromDi, children: children }));
735
764
  };
736
765
 
737
- function renderReact({ pageService, di }, context) {
766
+ function renderReact({ di }, context) {
738
767
  const serverState = typeof window !== 'undefined' ? context.getState() : undefined;
739
- return (jsxRuntime.jsx(state.Provider, { context: context, serverState: serverState, children: jsxRuntime.jsx(react$1.DIContext.Provider, { value: di, children: jsxRuntime.jsx(PageErrorBoundary, { pageService: pageService, children: jsxRuntime.jsx(Root, { pageService: pageService }) }) }) }));
768
+ return (jsxRuntime.jsx(state.Provider, { context: context, serverState: serverState, children: jsxRuntime.jsx(react$1.DIContext.Provider, { value: di, children: jsxRuntime.jsx(PageErrorBoundary, { children: jsxRuntime.jsx(Root, {}) }) }) }));
740
769
  }
741
770
 
742
771
  const RENDER_TIMEOUT = 500;
@@ -760,8 +789,7 @@ class HtmlWritable extends stream.Writable {
760
789
  }
761
790
  class ReactRenderServer {
762
791
  // eslint-disable-next-line sort-class-members/sort-class-members
763
- constructor({ pageService, context, customRender, extendRender, di, renderMode, logger }) {
764
- this.pageService = pageService;
792
+ constructor({ context, customRender, extendRender, di, renderMode, logger }) {
765
793
  this.context = context;
766
794
  this.customRender = customRender;
767
795
  this.extendRender = extendRender;
@@ -771,7 +799,7 @@ class ReactRenderServer {
771
799
  }
772
800
  render(extractor) {
773
801
  var _a;
774
- let renderResult = renderReact({ pageService: this.pageService, di: this.di }, this.context);
802
+ let renderResult = renderReact({ di: this.di }, this.context);
775
803
  each__default["default"]((render) => {
776
804
  renderResult = render(renderResult);
777
805
  }, (_a = this.extendRender) !== null && _a !== void 0 ? _a : []);
@@ -823,6 +851,7 @@ class ReactRenderServer {
823
851
  }
824
852
  }
825
853
 
854
+ const RenderChildrenComponent = ({ children }) => children;
826
855
  let LayoutModule = class LayoutModule {
827
856
  };
828
857
  LayoutModule = tslib.__decorate([
@@ -841,7 +870,10 @@ LayoutModule = tslib.__decorate([
841
870
  {
842
871
  provide: 'componentDefaultList',
843
872
  multi: true,
844
- useFactory: (components) => components,
873
+ useFactory: (components) => ({
874
+ ...components,
875
+ nestedLayoutDefault: RenderChildrenComponent,
876
+ }),
845
877
  deps: {
846
878
  layoutDefault: tokensRender.DEFAULT_LAYOUT_COMPONENT,
847
879
  footerDefault: { token: tokensRender.DEFAULT_FOOTER_COMPONENT, optional: true },
@@ -1001,7 +1033,6 @@ exports.RenderModule = RenderModule_1 = tslib.__decorate([
1001
1033
  useClass: ReactRenderServer,
1002
1034
  deps: {
1003
1035
  context: tokensCommon.CONTEXT_TOKEN,
1004
- pageService: tokensRouter.PAGE_SERVICE_TOKEN,
1005
1036
  customRender: { token: tokensRender.CUSTOM_RENDER, optional: true },
1006
1037
  extendRender: { token: tokensRender.EXTEND_RENDER, optional: true },
1007
1038
  di: core.DI_TOKEN,
@@ -1,4 +1,4 @@
1
- declare type AnyError = Error & {
1
+ type AnyError = Error & {
2
2
  [key: string]: any;
3
3
  };
4
4
  export interface SerializedError {
@@ -8,7 +8,7 @@ export interface SerializedError {
8
8
  }
9
9
  export declare function serializeError(error: AnyError): SerializedError;
10
10
  export declare function deserializeError(serializedError: SerializedError): AnyError;
11
- export declare type IPageErrorStore = SerializedError | null;
11
+ export type IPageErrorStore = SerializedError | null;
12
12
  export declare const setPageErrorEvent: import("@tramvai/types-actions-state-context").EventCreator1<AnyError, AnyError>;
13
13
  export declare const PageErrorStore: import("@tramvai/state").Reducer<SerializedError, "pageError">;
14
14
  export {};
@@ -1,4 +1,4 @@
1
- declare type RenderModuleConfig = {
1
+ type RenderModuleConfig = {
2
2
  polyfillCondition?: string;
3
3
  /**
4
4
  * @deprecated tramvai will automatically detect React version, and use hydrateRoot API for 18+ version
@@ -1,5 +1,5 @@
1
1
  import { getDiWrapper } from '@tramvai/test-helpers';
2
- declare type Options = Parameters<typeof getDiWrapper>[0];
2
+ type Options = Parameters<typeof getDiWrapper>[0];
3
3
  export declare const testPageResources: (options: Options) => {
4
4
  render: () => {
5
5
  parsed: import("node-html-parser").HTMLElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/module-render",
3
- "version": "2.39.3",
3
+ "version": "2.44.2",
4
4
  "description": "",
5
5
  "browser": "lib/browser.js",
6
6
  "main": "lib/server.js",
@@ -26,13 +26,13 @@
26
26
  "@tinkoff/htmlpagebuilder": "0.5.5",
27
27
  "@tinkoff/layout-factory": "0.3.4",
28
28
  "@tinkoff/url": "0.8.4",
29
- "@tinkoff/user-agent": "0.4.89",
30
- "@tramvai/module-client-hints": "2.39.3",
31
- "@tramvai/module-router": "2.39.3",
32
- "@tramvai/react": "2.39.3",
29
+ "@tinkoff/user-agent": "0.4.99",
30
+ "@tramvai/module-client-hints": "2.44.2",
31
+ "@tramvai/module-router": "2.44.2",
32
+ "@tramvai/react": "2.44.2",
33
33
  "@tramvai/safe-strings": "0.5.5",
34
- "@tramvai/tokens-render": "2.39.3",
35
- "@tramvai/experiments": "2.39.3",
34
+ "@tramvai/tokens-render": "2.44.2",
35
+ "@tramvai/experiments": "2.44.2",
36
36
  "@types/loadable__server": "^5.12.6",
37
37
  "node-fetch": "^2.6.1"
38
38
  },
@@ -40,14 +40,14 @@
40
40
  "@tinkoff/dippy": "0.8.9",
41
41
  "@tinkoff/utils": "^2.1.2",
42
42
  "@tinkoff/react-hooks": "0.1.4",
43
- "@tramvai/cli": "2.39.3",
44
- "@tramvai/core": "2.39.3",
45
- "@tramvai/module-common": "2.39.3",
46
- "@tramvai/state": "2.39.3",
47
- "@tramvai/test-helpers": "2.39.3",
48
- "@tramvai/tokens-common": "2.39.3",
49
- "@tramvai/tokens-router": "2.39.3",
50
- "@tramvai/tokens-server-private": "2.39.3",
43
+ "@tramvai/cli": "2.44.2",
44
+ "@tramvai/core": "2.44.2",
45
+ "@tramvai/module-common": "2.44.2",
46
+ "@tramvai/state": "2.44.2",
47
+ "@tramvai/test-helpers": "2.44.2",
48
+ "@tramvai/tokens-common": "2.44.2",
49
+ "@tramvai/tokens-router": "2.44.2",
50
+ "@tramvai/tokens-server-private": "2.44.2",
51
51
  "express": "^4.17.1",
52
52
  "prop-types": "^15.6.2",
53
53
  "react": ">=16.14.0",