Skip to content

Commit

Permalink
feat: new TRenderEngineProvider and RenderHTMLFragment components
Browse files Browse the repository at this point in the history
These components are meant to be used together in order to avoid
re-instantiating transient render engines for each instance of
`RenderHTML`, which is a costy operation when performed many times. This
is especially useful for apps using many instances, such as chat apps.

For this to work, mount a `TRenderEngineProvider` component near the
root of your application, and pass all it all props it support (see
typescript definition files for that purpose). Down in the render tree,
mount as many `RenderHTMLFragment` components as you wish, they will all
share the same `TRenderEngine` instance.
  • Loading branch information
jsamr committed Feb 9, 2021
1 parent f5dd83c commit 38176ed
Show file tree
Hide file tree
Showing 13 changed files with 396 additions and 242 deletions.
215 changes: 19 additions & 196 deletions packages/render-html/src/RenderHTML.tsx
Original file line number Diff line number Diff line change
@@ -1,202 +1,25 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Dimensions, Platform } from 'react-native';
import { RenderResolvedHTMLProps, RenderHTMLProps } from './shared-types';
import useTTree from './hooks/useTTree';
import SharedPropsContext, {
defaultSharedPropsContext
} from './context/SharedPropsContext';
import TChildrenRenderersContext from './context/TChildrenRendererContext';
import TNodeChildrenRenderer from './TNodeChildrenRenderer';
import RenderHTMLDebug from './RenderHTMLDebug';
import SourceLoader from './SourceLoader';
import RenderRegistryProvider from './context/RenderRegistryProvider';
import TChildrenRenderer from './TChildrenRenderer';
import TDocumentRenderer from './TDocumentRenderer';
import React from 'react';

export type RenderHTMLPropTypes = Record<keyof RenderHTMLProps, any>;
import { RenderHTMLProps } from './shared-types';

const propTypes: RenderHTMLPropTypes = {
renderers: PropTypes.object.isRequired,
defaultTextProps: PropTypes.object,
defaultViewProps: PropTypes.object,
source: PropTypes.oneOfType([
PropTypes.shape({
html: PropTypes.string.isRequired,
baseUrl: PropTypes.string
}),
PropTypes.shape({
uri: PropTypes.string.isRequired,
method: PropTypes.string,
body: PropTypes.any,
headers: PropTypes.object
})
]),
enableCSSInlineProcessing: PropTypes.bool,
enableUserAgentStyles: PropTypes.bool,
enableExperimentalMarginCollapsing: PropTypes.bool,
idsStyles: PropTypes.object,
remoteErrorView: PropTypes.func,
remoteLoadingView: PropTypes.func,
ignoredTags: PropTypes.array.isRequired,
ignoredStyles: PropTypes.array.isRequired,
allowedStyles: PropTypes.array,
htmlParserOptions: PropTypes.object,
debug: PropTypes.bool.isRequired,
listsPrefixesRenderers: PropTypes.object,
ignoreDOMNode: PropTypes.func,
alterDOMData: PropTypes.func,
alterDOMChildren: PropTypes.func,
alterDOMElement: PropTypes.func,
tagsStyles: PropTypes.object,
classesStyles: PropTypes.object,
onLinkPress: PropTypes.func,
computeEmbeddedMaxWidth: PropTypes.func,
contentWidth: PropTypes.number,
enableExperimentalPercentWidth: PropTypes.bool,
imagesInitialDimensions: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number
}),
emSize: PropTypes.number.isRequired,
baseStyle: PropTypes.object,
renderersProps: PropTypes.object,
onTTreeChange: PropTypes.func,
onHTMLLoaded: PropTypes.func,
systemFonts: PropTypes.arrayOf(PropTypes.string),
fallbackFonts: PropTypes.shape({
serif: PropTypes.string,
'sans-serif': PropTypes.string,
monospace: PropTypes.string
}),
triggerTREInvalidationPropNames: PropTypes.arrayOf(PropTypes.string),
WebView: PropTypes.any,
defaultWebViewProps: PropTypes.object,
onDocumentMetadataLoaded: PropTypes.func
};
import TRenderEngineProvider from './TRenderEngineProvider';
import RenderHTMLFragment from './RenderHTMLFragment';

const defaultProps: {
[k in keyof RenderHTMLProps]?: RenderHTMLProps[k];
} = {
...defaultSharedPropsContext,
htmlParserOptions: {
decodeEntities: true
},
emSize: 14,
ignoredTags: [],
ignoredStyles: [],
baseStyle: { fontSize: 14 },
tagsStyles: {},
classesStyles: {},
enableUserAgentStyles: true,
enableCSSInlineProcessing: true,
renderers: {},
fallbackFonts: {
'sans-serif': Platform.select({ ios: 'system', default: 'sans-serif' }),
monospace: Platform.select({ ios: 'Menlo', default: 'monospace' }),
serif: Platform.select({ ios: 'Times New Roman', default: 'serif' })
},
systemFonts: Platform.select({
default: [],
ios: [
'San Francisco',
'Arial',
'ArialHebrew',
'Avenir',
'Baskerville',
'Bodoni 72',
'Bradley Hand',
'Chalkboard SE',
'Cochin',
'Copperplate',
'Courier',
'Courier New',
'Damascus',
'Didot',
'Futura',
'Geeza Pro',
'Georgia',
'Gill Sans',
'Helvetica',
'Helvetica Neue',
'Hiragino Sans',
'Hoefler Text',
'Iowan Old Style',
'Kailasa',
'Khmer Sangam MN',
'Marker Felt',
'Menlo',
'Mishafi',
'Noteworthy',
'Optima',
'Palatino',
'Papyrus',
'Savoye LET',
'Symbol',
'Thonburi',
'Times New Roman',
'Trebuchet MS',
'Verdana',
'Zapf Dingbats',
'Zapfino'
],
android: [
'Roboto',
'notoserif',
'sans-serif-light',
'sans-serif-thin',
'sans-serif-medium'
]
}),
triggerTREInvalidationPropNames: [],
debug: __DEV__,
contentWidth: undefined
};

function RenderResolvedHTML(props: RenderResolvedHTMLProps) {
const ttree = useTTree(props);
return (
<TDocumentRenderer
tdoc={ttree}
baseUrl={props.baseUrl}
onDocumentMetadataLoaded={props.onDocumentMetadataLoaded}
/>
);
}

export default function RenderHTML({
defaultTextProps,
...props
}: RenderHTMLProps) {
const normalizedProps = {
contentWidth: Dimensions.get('window').width,
...props,
defaultTextProps: { ...defaultProps.defaultTextProps, ...defaultTextProps }
};
/**
* Render HTML text in native views!
*
* @remarks - If your application uses many instances of this component, you
* should share the render engine across those instances via the
* `TRenderEngineProvier` component, and render the HTML with
* `RenderHTMLFragment` instead. That should significantly increase
* performance.
*
* @param props - Props for this component.
*/
export default function RenderHTML(props: RenderHTMLProps) {
return (
<RenderHTMLDebug {...props}>
<RenderRegistryProvider renderers={normalizedProps.renderers}>
<SharedPropsContext.Provider
value={normalizedProps as Required<RenderHTMLProps>}>
<TChildrenRenderersContext.Provider
value={useMemo(
() => ({
TChildrenRenderer,
TNodeChildrenRenderer
}),
[]
)}>
<SourceLoader {...normalizedProps}>
{(resolvedProps) => (
<RenderResolvedHTML {...normalizedProps} {...resolvedProps} />
)}
</SourceLoader>
</TChildrenRenderersContext.Provider>
</SharedPropsContext.Provider>
</RenderRegistryProvider>
</RenderHTMLDebug>
<TRenderEngineProvider {...props}>
{React.createElement(RenderHTMLFragment, props)}
</TRenderEngineProvider>
);
}

RenderHTML.defaultProps = defaultProps;
RenderHTML.propTypes = propTypes;
123 changes: 123 additions & 0 deletions packages/render-html/src/RenderHTMLFragment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';

import {
RenderResolvedHTMLProps,
ResolvedResourceProps,
RenderHTMLFragmentProps
} from './shared-types';
import useTTree from './hooks/useTTree';
import SharedPropsContext, {
defaultSharedPropsContext
} from './context/SharedPropsContext';
import TChildrenRenderersContext from './context/TChildrenRendererContext';
import TNodeChildrenRenderer from './TNodeChildrenRenderer';
import RenderHTMLFragmentDebug from './RenderHTMLFragmentDebug';
import SourceLoader from './SourceLoader';
import TChildrenRenderer from './TChildrenRenderer';
import TDocumentRenderer from './TDocumentRenderer';
import selectSharedProps from './helpers/selectSharedProps';

export type RenderHTMLFragmentPropTypes = Record<
keyof RenderHTMLFragmentProps,
any
>;

export const renderHtmlFragmentPropTypes: RenderHTMLFragmentPropTypes = {
defaultTextProps: PropTypes.object,
defaultViewProps: PropTypes.object,
source: PropTypes.oneOfType([
PropTypes.shape({
html: PropTypes.string.isRequired,
baseUrl: PropTypes.string
}),
PropTypes.shape({
uri: PropTypes.string.isRequired,
method: PropTypes.string,
body: PropTypes.any,
headers: PropTypes.object
})
]),
enableExperimentalMarginCollapsing: PropTypes.bool,
remoteErrorView: PropTypes.func,
remoteLoadingView: PropTypes.func,
debug: PropTypes.bool.isRequired,
listsPrefixesRenderers: PropTypes.object,
onLinkPress: PropTypes.func,
computeEmbeddedMaxWidth: PropTypes.func,
contentWidth: PropTypes.number,
enableExperimentalPercentWidth: PropTypes.bool,
imagesInitialDimensions: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number
}),
renderersProps: PropTypes.object,
onTTreeChange: PropTypes.func,
onHTMLLoaded: PropTypes.func,
WebView: PropTypes.any,
defaultWebViewProps: PropTypes.object
};

export const renderHTMLFragmentDefaultProps: {
[k in keyof RenderHTMLFragmentProps]?: RenderHTMLFragmentProps[k];
} = {
...defaultSharedPropsContext,
contentWidth: undefined
};

function RenderResolvedHTML(props: RenderResolvedHTMLProps) {
const ttree = useTTree(props);
return (
<TDocumentRenderer
tdoc={ttree}
baseUrl={props.baseUrl}
onDocumentMetadataLoaded={props.onDocumentMetadataLoaded}
/>
);
}

const renderResolved = (resolvedProps: ResolvedResourceProps) => (
<RenderResolvedHTML {...resolvedProps} />
);

/**
* Render a HTML snippet, given that there is a `TRenderEngineProvider` up in
* the render tree.
*
* @param props - Props for this component.
*/
export default function RenderHTMLFragment(props: RenderHTMLFragmentProps) {
const {
source,
onHTMLLoaded,
remoteErrorView,
remoteLoadingView,
...remainingProps
} = props;
const sourceLoaderProps = {
source,
onHTMLLoaded,
remoteErrorView,
remoteLoadingView,
children: renderResolved
};
return (
<RenderHTMLFragmentDebug {...props}>
<SharedPropsContext.Provider value={selectSharedProps(remainingProps)}>
<TChildrenRenderersContext.Provider
value={useMemo(
() => ({
TChildrenRenderer,
TNodeChildrenRenderer
}),
[]
)}>
{React.createElement(SourceLoader, sourceLoaderProps)}
</TChildrenRenderersContext.Provider>
</SharedPropsContext.Provider>
</RenderHTMLFragmentDebug>
);
}

RenderHTMLFragment.defaultProps = renderHTMLFragmentDefaultProps;
RenderHTMLFragment.propTypes = renderHtmlFragmentPropTypes;
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import React, { Fragment } from 'react';
import { PropsWithChildren } from 'react';
import lookupRecord from './helpers/lookupRecord';
import { RenderHTMLProps } from './shared-types';

export function RenderHTMLProd(props: PropsWithChildren<RenderHTMLProps>) {
return <Fragment>{props.children}</Fragment>;
}
import { RenderHTMLFragmentProps } from './shared-types';

export const messages = {
outdatedUriProp:
Expand All @@ -25,8 +21,8 @@ export const messages = {
'https://reactnative.dev/docs/usewindowdimensions'
};

const RenderHTMLDebug = function RenderHTMLDebug(
props: PropsWithChildren<RenderHTMLProps>
const RenderHTMLFragmentDebug = function RenderHTMLDebug(
props: PropsWithChildren<RenderHTMLFragmentProps>
) {
if (__DEV__) {
if (typeof props.contentWidth !== 'number') {
Expand All @@ -45,4 +41,4 @@ const RenderHTMLDebug = function RenderHTMLDebug(
return <Fragment>{props.children}</Fragment>;
};

export default RenderHTMLDebug;
export default RenderHTMLFragmentDebug;
Loading

0 comments on commit 38176ed

Please sign in to comment.