Skip to content

Commit

Permalink
refactor: lean render tree with fewer React components
Browse files Browse the repository at this point in the history
  • Loading branch information
jsamr committed Oct 26, 2021
1 parent 636e823 commit 4c63e99
Show file tree
Hide file tree
Showing 19 changed files with 322 additions and 292 deletions.
37 changes: 0 additions & 37 deletions packages/render-html/src/TBlockRenderer.tsx

This file was deleted.

79 changes: 4 additions & 75 deletions packages/render-html/src/TChildrenRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,84 +1,13 @@
import React, { ReactElement } from 'react';
import { TNode } from '@native-html/transient-render-engine';
import TNodeRenderer from './TNodeRenderer';
import { FunctionComponent } from 'react';
import { TChildrenRendererProps } from './shared-types';
import getCollapsedMarginTop from './helpers/getCollapsedMarginTop';

function isCollapsible(tnode: TNode) {
return tnode.type === 'block' || tnode.type === 'phrasing';
}

/**
* Compute top collapsed margin for the nth {@link TNode}-child of a list of
* TNodes.
*
* @param n - The index for which the top margin should be collapsed.
* @param tchildren - The list of {@link TNode} children.
* @returns `null` when no margin collapsing should apply, a number otherwise.
* @public
*/
export function collapseTopMarginForChild(
n: number,
tchildren: readonly TNode[]
): number | null {
const childTnode = tchildren[n];
if (isCollapsible(childTnode) && n > 0 && isCollapsible(tchildren[n - 1])) {
return getCollapsedMarginTop(tchildren[n - 1], childTnode);
}
return null;
}

const mapCollapsibleChildren = (
propsForChildren: TChildrenRendererProps['propsForChildren'],
renderChild: TChildrenRendererProps['renderChild'],
disableMarginCollapsing: boolean | undefined,
childTnode: TNode,
n: number,
tchildren: readonly TNode[]
) => {
const collapsedMarginTop = disableMarginCollapsing
? null
: collapseTopMarginForChild(n, tchildren);
const propsFromParent = { ...propsForChildren, collapsedMarginTop };
const key = childTnode.nodeIndex;
const childElement = React.createElement(TNodeRenderer, {
propsFromParent,
tnode: childTnode,
key,
renderIndex: n,
renderLength: tchildren.length
});
return typeof renderChild === 'function'
? renderChild({
key,
childElement,
index: n,
childTnode,
propsFromParent
})
: childElement;
};
import renderChildren from './renderChildren';

/**
* A component to render collections of tnodes.
* Especially useful when used with {@link useTNodeChildrenProps}.
*/
function TChildrenRenderer({
tchildren,
propsForChildren,
disableMarginCollapsing,
renderChild
}: TChildrenRendererProps): ReactElement {
const elements = tchildren.map(
mapCollapsibleChildren.bind(
null,
propsForChildren,
renderChild,
disableMarginCollapsing
)
);
return <>{elements}</>;
}
const TChildrenRenderer: FunctionComponent<TChildrenRendererProps> =
renderChildren.bind(null);

export const tchildrenRendererDefaultProps: Pick<
TChildrenRendererProps,
Expand Down
18 changes: 7 additions & 11 deletions packages/render-html/src/TNodeChildrenRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { ReactElement } from 'react';
import { ReactElement } from 'react';
import { TNode } from '@native-html/transient-render-engine';
import { useSharedProps } from './context/SharedPropsProvider';
import TChildrenRenderer, {
tchildrenRendererDefaultProps
} from './TChildrenRenderer';
import { tchildrenRendererDefaultProps } from './TChildrenRenderer';
import {
TChildrenRendererProps,
TNodeChildrenRendererProps
} from './shared-types';
import renderChildren from './renderChildren';

function isCollapsible(tnode: TNode) {
return tnode.type === 'block' || tnode.type === 'phrasing';
Expand Down Expand Up @@ -58,12 +57,6 @@ export function useTNodeChildrenProps({
};
}

const TNodeWithChildrenRenderer = function TNodeWithChildrenRenderer(
props: TNodeChildrenRendererProps
) {
return React.createElement(TChildrenRenderer, useTNodeChildrenProps(props));
};

/**
* A component to render all children of a {@link TNode}.
*/
Expand All @@ -74,7 +67,10 @@ function TNodeChildrenRenderer(
// see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544
return props.tnode.data as unknown as ReactElement;
}
return React.createElement(TNodeWithChildrenRenderer, props);
// A tnode type will never change. We can safely
// ignore the non-conditional rule of hooks.
// eslint-disable-next-line react-hooks/rules-of-hooks
return renderChildren(useTNodeChildrenProps(props));
}

/**
Expand Down
132 changes: 107 additions & 25 deletions packages/render-html/src/TNodeRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import React, { memo, ReactElement } from 'react';
import TBlockRenderer from './TBlockRenderer';
import TPhrasingRenderer from './TPhrasingRenderer';
import TTextRenderer from './TTextRenderer';
import { TNodeRendererProps } from './shared-types';
import { TDefaultRenderer, TNodeRendererProps } from './shared-types';
import { useSharedProps } from './context/SharedPropsProvider';
import {
TText,
TBlock,
TNode,
TPhrasing
} from '@native-html/transient-render-engine';
import useAssembledCommonProps from './hooks/useAssembledCommonProps';
import { useTNodeChildrenRenderer } from './context/TChildrenRendererContext';
import renderTextualContent from './renderTextualContent';
import { useRendererRegistry } from './context/RenderRegistryProvider';
import renderBlockContent from './renderBlockContent';
import renderEmptyContent from './renderEmptyContent';

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

const TDefaultBlockRenderer: TDefaultRenderer<TBlock> =
renderBlockContent.bind(null);

TDefaultBlockRenderer.displayName = 'TDefaultBlockRenderer';

const TDefaultPhrasingRenderer: TDefaultRenderer<TPhrasing> =
renderTextualContent.bind(null);

TDefaultPhrasingRenderer.displayName = 'TDefaultPhrasingRenderer';

const TDefaultTextRenderer: TDefaultRenderer<TText> =
renderTextualContent.bind(null);

TDefaultTextRenderer.displayName = 'TDefaultTextRenderer';

function isGhostTNode(tnode: TNode) {
return (
(tnode.type === 'text' && (tnode.data === '' || tnode.data === ' ')) ||
tnode.type === 'empty'
);
}

/**
* A component to render any {@link TNode}.
*/
Expand All @@ -15,33 +46,78 @@ const TNodeRenderer = memo(function MemoizedTNodeRenderer(
): ReactElement | null {
const { tnode } = props;
const sharedProps = useSharedProps();
const renderRegistry = useRendererRegistry();
const TNodeChildrenRenderer = useTNodeChildrenRenderer();
const tnodeProps = {
...props,
TNodeChildrenRenderer,
sharedProps
};
if (tnode.type === 'block' || tnode.type === 'document') {
return React.createElement(TBlockRenderer, tnodeProps);
}
if (tnode.type === 'phrasing') {
return React.createElement(TPhrasingRenderer, tnodeProps);
}
if (tnode.type === 'text') {
return React.createElement(TTextRenderer, tnodeProps);
}
if (typeof __DEV__ === 'boolean' && __DEV__ && tnode.type === 'empty') {
if (tnode.isUnregistered) {
console.warn(
`There is no custom renderer registered for tag "${tnode.tagName}" which is not part of the HTML5 standard. The tag will not be rendered.` +
' 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.'
);
} else if (tnode.tagName !== 'head') {
console.warn(
`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".` +
' If you don\'t want this tag to be rendered, add it to "ignoredTags" prop array.'
const renderer =
tnode.type === 'block' || tnode.type === 'document'
? TDefaultBlockRenderer
: tnode.type === 'text'
? TDefaultTextRenderer
: tnode.type === 'phrasing'
? TDefaultPhrasingRenderer
: renderEmptyContent;

const { assembledProps, Renderer } = useAssembledCommonProps(
tnodeProps,
renderer as any
);
switch (tnode.type) {
case 'empty':
return renderEmptyContent(assembledProps);
case 'text':
const InternalTextRenderer = renderRegistry.getInternalTextRenderer(
props.tnode.tagName
);
}

if (InternalTextRenderer) {
return React.createElement(InternalTextRenderer, tnodeProps);
}
// If ghost line prevention is enabled and the text data is empty, render
// nothing to avoid React Native painting a 20px height line.
// See also https://git.io/JErwX
if (
tnodeProps.tnode.data === '' &&
tnodeProps.sharedProps.enableExperimentalGhostLinesPrevention
) {
return null;
}
break;
case 'phrasing':
// When a TPhrasing node is anonymous and has only one child, its
// rendering amounts to rendering its only child.
if (
tnodeProps.sharedProps.bypassAnonymousTPhrasingNodes &&
tnodeProps.tnode.tagName == null &&
tnodeProps.tnode.children.length <= 1
) {
return React.createElement(TNodeChildrenRenderer, {
tnode: props.tnode
});
}
// If ghost line prevention is enabled and the tnode is an anonymous empty
// phrasing node, render nothing to avoid React Native painting a 20px
// height line. See also https://git.io/JErwX
if (
tnodeProps.sharedProps.enableExperimentalGhostLinesPrevention &&
tnodeProps.tnode.tagName == null &&
tnodeProps.tnode.children.every(isGhostTNode)
) {
return null;
}
break;
}
return null;
const renderFn =
tnode.type === 'block' || tnode.type === 'document'
? renderBlockContent
: renderTextualContent;
return Renderer === null
? renderFn(assembledProps)
: React.createElement(Renderer as any, assembledProps);
});

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

export {
TDefaultBlockRenderer,
TDefaultPhrasingRenderer,
TDefaultTextRenderer
};

export default TNodeRenderer;
62 changes: 0 additions & 62 deletions packages/render-html/src/TPhrasingRenderer.ts

This file was deleted.

Loading

0 comments on commit 4c63e99

Please sign in to comment.