Skip to content

Commit 4c63e99

Browse files
committed
refactor: lean render tree with fewer React components
1 parent 636e823 commit 4c63e99

19 files changed

+322
-292
lines changed

packages/render-html/src/TBlockRenderer.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.

packages/render-html/src/TChildrenRenderer.tsx

Lines changed: 4 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,13 @@
1-
import React, { ReactElement } from 'react';
2-
import { TNode } from '@native-html/transient-render-engine';
3-
import TNodeRenderer from './TNodeRenderer';
1+
import { FunctionComponent } from 'react';
42
import { TChildrenRendererProps } from './shared-types';
5-
import getCollapsedMarginTop from './helpers/getCollapsedMarginTop';
6-
7-
function isCollapsible(tnode: TNode) {
8-
return tnode.type === 'block' || tnode.type === 'phrasing';
9-
}
10-
11-
/**
12-
* Compute top collapsed margin for the nth {@link TNode}-child of a list of
13-
* TNodes.
14-
*
15-
* @param n - The index for which the top margin should be collapsed.
16-
* @param tchildren - The list of {@link TNode} children.
17-
* @returns `null` when no margin collapsing should apply, a number otherwise.
18-
* @public
19-
*/
20-
export function collapseTopMarginForChild(
21-
n: number,
22-
tchildren: readonly TNode[]
23-
): number | null {
24-
const childTnode = tchildren[n];
25-
if (isCollapsible(childTnode) && n > 0 && isCollapsible(tchildren[n - 1])) {
26-
return getCollapsedMarginTop(tchildren[n - 1], childTnode);
27-
}
28-
return null;
29-
}
30-
31-
const mapCollapsibleChildren = (
32-
propsForChildren: TChildrenRendererProps['propsForChildren'],
33-
renderChild: TChildrenRendererProps['renderChild'],
34-
disableMarginCollapsing: boolean | undefined,
35-
childTnode: TNode,
36-
n: number,
37-
tchildren: readonly TNode[]
38-
) => {
39-
const collapsedMarginTop = disableMarginCollapsing
40-
? null
41-
: collapseTopMarginForChild(n, tchildren);
42-
const propsFromParent = { ...propsForChildren, collapsedMarginTop };
43-
const key = childTnode.nodeIndex;
44-
const childElement = React.createElement(TNodeRenderer, {
45-
propsFromParent,
46-
tnode: childTnode,
47-
key,
48-
renderIndex: n,
49-
renderLength: tchildren.length
50-
});
51-
return typeof renderChild === 'function'
52-
? renderChild({
53-
key,
54-
childElement,
55-
index: n,
56-
childTnode,
57-
propsFromParent
58-
})
59-
: childElement;
60-
};
3+
import renderChildren from './renderChildren';
614

625
/**
636
* A component to render collections of tnodes.
647
* Especially useful when used with {@link useTNodeChildrenProps}.
658
*/
66-
function TChildrenRenderer({
67-
tchildren,
68-
propsForChildren,
69-
disableMarginCollapsing,
70-
renderChild
71-
}: TChildrenRendererProps): ReactElement {
72-
const elements = tchildren.map(
73-
mapCollapsibleChildren.bind(
74-
null,
75-
propsForChildren,
76-
renderChild,
77-
disableMarginCollapsing
78-
)
79-
);
80-
return <>{elements}</>;
81-
}
9+
const TChildrenRenderer: FunctionComponent<TChildrenRendererProps> =
10+
renderChildren.bind(null);
8211

8312
export const tchildrenRendererDefaultProps: Pick<
8413
TChildrenRendererProps,

packages/render-html/src/TNodeChildrenRenderer.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import React, { ReactElement } from 'react';
1+
import { ReactElement } from 'react';
22
import { TNode } from '@native-html/transient-render-engine';
33
import { useSharedProps } from './context/SharedPropsProvider';
4-
import TChildrenRenderer, {
5-
tchildrenRendererDefaultProps
6-
} from './TChildrenRenderer';
4+
import { tchildrenRendererDefaultProps } from './TChildrenRenderer';
75
import {
86
TChildrenRendererProps,
97
TNodeChildrenRendererProps
108
} from './shared-types';
9+
import renderChildren from './renderChildren';
1110

1211
function isCollapsible(tnode: TNode) {
1312
return tnode.type === 'block' || tnode.type === 'phrasing';
@@ -58,12 +57,6 @@ export function useTNodeChildrenProps({
5857
};
5958
}
6059

61-
const TNodeWithChildrenRenderer = function TNodeWithChildrenRenderer(
62-
props: TNodeChildrenRendererProps
63-
) {
64-
return React.createElement(TChildrenRenderer, useTNodeChildrenProps(props));
65-
};
66-
6760
/**
6861
* A component to render all children of a {@link TNode}.
6962
*/
@@ -74,7 +67,10 @@ function TNodeChildrenRenderer(
7467
// see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544
7568
return props.tnode.data as unknown as ReactElement;
7669
}
77-
return React.createElement(TNodeWithChildrenRenderer, props);
70+
// A tnode type will never change. We can safely
71+
// ignore the non-conditional rule of hooks.
72+
// eslint-disable-next-line react-hooks/rules-of-hooks
73+
return renderChildren(useTNodeChildrenProps(props));
7874
}
7975

8076
/**
Lines changed: 107 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,43 @@
11
import React, { memo, ReactElement } from 'react';
2-
import TBlockRenderer from './TBlockRenderer';
3-
import TPhrasingRenderer from './TPhrasingRenderer';
4-
import TTextRenderer from './TTextRenderer';
5-
import { TNodeRendererProps } from './shared-types';
2+
import { TDefaultRenderer, TNodeRendererProps } from './shared-types';
63
import { useSharedProps } from './context/SharedPropsProvider';
4+
import {
5+
TText,
6+
TBlock,
7+
TNode,
8+
TPhrasing
9+
} from '@native-html/transient-render-engine';
10+
import useAssembledCommonProps from './hooks/useAssembledCommonProps';
11+
import { useTNodeChildrenRenderer } from './context/TChildrenRendererContext';
12+
import renderTextualContent from './renderTextualContent';
13+
import { useRendererRegistry } from './context/RenderRegistryProvider';
14+
import renderBlockContent from './renderBlockContent';
15+
import renderEmptyContent from './renderEmptyContent';
716

817
export type { TNodeRendererProps } from './shared-types';
918

19+
const TDefaultBlockRenderer: TDefaultRenderer<TBlock> =
20+
renderBlockContent.bind(null);
21+
22+
TDefaultBlockRenderer.displayName = 'TDefaultBlockRenderer';
23+
24+
const TDefaultPhrasingRenderer: TDefaultRenderer<TPhrasing> =
25+
renderTextualContent.bind(null);
26+
27+
TDefaultPhrasingRenderer.displayName = 'TDefaultPhrasingRenderer';
28+
29+
const TDefaultTextRenderer: TDefaultRenderer<TText> =
30+
renderTextualContent.bind(null);
31+
32+
TDefaultTextRenderer.displayName = 'TDefaultTextRenderer';
33+
34+
function isGhostTNode(tnode: TNode) {
35+
return (
36+
(tnode.type === 'text' && (tnode.data === '' || tnode.data === ' ')) ||
37+
tnode.type === 'empty'
38+
);
39+
}
40+
1041
/**
1142
* A component to render any {@link TNode}.
1243
*/
@@ -15,33 +46,78 @@ const TNodeRenderer = memo(function MemoizedTNodeRenderer(
1546
): ReactElement | null {
1647
const { tnode } = props;
1748
const sharedProps = useSharedProps();
49+
const renderRegistry = useRendererRegistry();
50+
const TNodeChildrenRenderer = useTNodeChildrenRenderer();
1851
const tnodeProps = {
1952
...props,
53+
TNodeChildrenRenderer,
2054
sharedProps
2155
};
22-
if (tnode.type === 'block' || tnode.type === 'document') {
23-
return React.createElement(TBlockRenderer, tnodeProps);
24-
}
25-
if (tnode.type === 'phrasing') {
26-
return React.createElement(TPhrasingRenderer, tnodeProps);
27-
}
28-
if (tnode.type === 'text') {
29-
return React.createElement(TTextRenderer, tnodeProps);
30-
}
31-
if (typeof __DEV__ === 'boolean' && __DEV__ && tnode.type === 'empty') {
32-
if (tnode.isUnregistered) {
33-
console.warn(
34-
`There is no custom renderer registered for tag "${tnode.tagName}" which is not part of the HTML5 standard. The tag will not be rendered.` +
35-
' If you don\'t want this tag to be rendered, add it to "ignoredTags" prop array. If you do, register an HTMLElementModel for this tag with "customHTMLElementModels" prop.'
36-
);
37-
} else if (tnode.tagName !== 'head') {
38-
console.warn(
39-
`The "${tnode.tagName}" tag is a valid HTML element but is not handled by this library. You must extend the default HTMLElementModel for this tag with "customHTMLElementModels" prop and make sure its content model is not set to "none".` +
40-
' If you don\'t want this tag to be rendered, add it to "ignoredTags" prop array.'
56+
const renderer =
57+
tnode.type === 'block' || tnode.type === 'document'
58+
? TDefaultBlockRenderer
59+
: tnode.type === 'text'
60+
? TDefaultTextRenderer
61+
: tnode.type === 'phrasing'
62+
? TDefaultPhrasingRenderer
63+
: renderEmptyContent;
64+
65+
const { assembledProps, Renderer } = useAssembledCommonProps(
66+
tnodeProps,
67+
renderer as any
68+
);
69+
switch (tnode.type) {
70+
case 'empty':
71+
return renderEmptyContent(assembledProps);
72+
case 'text':
73+
const InternalTextRenderer = renderRegistry.getInternalTextRenderer(
74+
props.tnode.tagName
4175
);
42-
}
76+
77+
if (InternalTextRenderer) {
78+
return React.createElement(InternalTextRenderer, tnodeProps);
79+
}
80+
// If ghost line prevention is enabled and the text data is empty, render
81+
// nothing to avoid React Native painting a 20px height line.
82+
// See also https://git.io/JErwX
83+
if (
84+
tnodeProps.tnode.data === '' &&
85+
tnodeProps.sharedProps.enableExperimentalGhostLinesPrevention
86+
) {
87+
return null;
88+
}
89+
break;
90+
case 'phrasing':
91+
// When a TPhrasing node is anonymous and has only one child, its
92+
// rendering amounts to rendering its only child.
93+
if (
94+
tnodeProps.sharedProps.bypassAnonymousTPhrasingNodes &&
95+
tnodeProps.tnode.tagName == null &&
96+
tnodeProps.tnode.children.length <= 1
97+
) {
98+
return React.createElement(TNodeChildrenRenderer, {
99+
tnode: props.tnode
100+
});
101+
}
102+
// If ghost line prevention is enabled and the tnode is an anonymous empty
103+
// phrasing node, render nothing to avoid React Native painting a 20px
104+
// height line. See also https://git.io/JErwX
105+
if (
106+
tnodeProps.sharedProps.enableExperimentalGhostLinesPrevention &&
107+
tnodeProps.tnode.tagName == null &&
108+
tnodeProps.tnode.children.every(isGhostTNode)
109+
) {
110+
return null;
111+
}
112+
break;
43113
}
44-
return null;
114+
const renderFn =
115+
tnode.type === 'block' || tnode.type === 'document'
116+
? renderBlockContent
117+
: renderTextualContent;
118+
return Renderer === null
119+
? renderFn(assembledProps)
120+
: React.createElement(Renderer as any, assembledProps);
45121
});
46122

47123
const defaultProps: Required<Pick<TNodeRendererProps<any>, 'propsFromParent'>> =
@@ -54,4 +130,10 @@ const defaultProps: Required<Pick<TNodeRendererProps<any>, 'propsFromParent'>> =
54130
// @ts-expect-error default props must be defined
55131
TNodeRenderer.defaultProps = defaultProps;
56132

133+
export {
134+
TDefaultBlockRenderer,
135+
TDefaultPhrasingRenderer,
136+
TDefaultTextRenderer
137+
};
138+
57139
export default TNodeRenderer;

packages/render-html/src/TPhrasingRenderer.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

0 commit comments

Comments
 (0)