react-native-render-html

repository·master·Indexed 25 days ago

https://github.com/meliorence/react-native-render-html

A pure JavaScript React Native component that renders HTML into 100% native views on iOS and Android, avoiding the use of WebViews. It features a Composite Rendering Architecture for optimization, support for custom renderers via React components, and system font configuration for CSS font-family matching. The current stable version is v6 (The Foundry).

Tokens
16.8K
Snippets
39
Records
75
Agent score
86%

What's inside react-native-render-html

  1. Understand the Transient Render Tree structure

    master

    The library uses an intermediary data structure called a Transient Render Tree to handle CSS whitespace collapsing and React Native constraints before final rendering. This tree is composed of TNode objects, which are categorized into four types:

    • TBlock: Represents block-level content. Children can be TBlock, TPhrasing, or TEmpty. Typically rendered as a React Native <View /> (or via custom renderers).
    • TPhrasing: Represents inline/phrasing content. Children can be TText, TPhrasing, or TEmpty. Typically rendered as a React Native <Text /> node, creating an inline formatting context.
    • TText: Represents raw text. It cannot have children and contains the actual string data.
    • TEmpty: Represents nodes that should not be rendered (e.g., <script>, <link>).
    interface TNode {
        type: 'block' | 'phrasing' | 'text' | 'empty';
        attributes: Record<string, string>;
        children: TNode[];
        isAnchor: boolean;
        isCollapsibleLeft(): boolean;
        isCollapsibleRight(): boolean;
        isWhitespace(): boolean;
        isEmpty(): boolean;
        trimLeft(): void;
        trimRight(): void;
        getFirstChild(): TNode | null;
        getLastChild(): TNode | null;
    }
    
    interface TBlock extends TNode {
        type: 'block';
        tagName?: string;
    }
    
    interface TPhrasing extends TNode {
        type: 'phrasing';
        tagName?: string;
    }
    
    interface TText extends TNode {
        type: 'text';
        tagName?: string;
        data: string;
    }
    
    interface TEmpty extends TBlock {
      type: 'empty';
      tagName?: string;
    }
  2. Write a Request For Comments (RFC)

    master

    An RFC is a proposition for new methods, behaviors, or API capabilities. RFCs must be written in AsciiDoc.

    A typical RFC structure should include:

    1. Problem Frame: The context and the issues the enhancement or feature addresses.
    2. Requirements: The behaviors required for the feature to be supported.

    You can find examples of existing RFCs in the rfc folder of the repository.

  3. Optimize rendering with Composite Rendering Architecture

    master

    To avoid the overhead of re-instantiating the Transient Render Engine (TRE) when rendering multiple HTML snippets, you can use the explicit 3-layer provider pattern. This allows you to share configuration via context:

    <TRenderEngineProvider>
      <RenderHTMLConfigProvider>
        <RenderHTMLSource source={{ html }} />
      </RenderHTMLConfigProvider>
    </TRenderEngineProvider>
  4. Set up Explicit Composite Rendering Architecture

    master

    To gain direct access to the DOM object (e.g., for extracting headings), use the explicit composite rendering architecture. This involves replacing the standard <RenderHTML /> component with <RenderHTMLSource /> and wrapping your application (or a specific subtree) in <TRenderEngineProvider /> and <RenderHTMLConfigProvider />.

    Key configuration props for <TRenderEngineProvider />:

    • ignoredDomTags: An array of tag names to ignore during parsing.
    • selectDomRoot: A function to select the specific DOM element to serve as the rendering root.

    Key configuration props for <RenderHTMLConfigProvider />:

    • enableExperimentalMarginCollapsing: A boolean to enable vertical margin collapsing.
    import * as React from 'react';
    import {
      RenderHTMLConfigProvider,
      TRenderEngineProvider,
      TRenderEngineConfig,
    } from 'react-native-render-html';
    import { findOne } from 'domutils';
    
    // Example: Select the <article> tag as the root
    const selectDomRoot: TRenderEngineConfig["selectDomRoot"] = (node) =>
      findOne((e) => e.name === "article", node.children, true);
    
    const ignoredDomTags = ["button"];
    
    export default function WebEngine({ children }: React.PropsWithChildren<{}>) {
      return (
        <TRenderEngineProvider
          ignoredDomTags={ignoredDomTags}
          selectDomRoot={selectDomRoot}>
          <RenderHTMLConfigProvider enableExperimentalMarginCollapsing>
            {children}
          </RenderHTMLConfigProvider>
        </TRenderEngineProvider>
      );
    }
  5. Initialize a React Native Blog project with Expo

    master

    To create a new project for a WebView-free blog app using Expo, TypeScript, and Yarn, follow these steps:

    1. Install the Expo CLI globally:
      npm install -g expo-cli
    2. Initialize the project using the blank TypeScript template:
      expo init rnrh-blog -t expo-template-blank-typescript --name Blog --yarn
    npm install -g expo-cli
    expo init rnrh-blog -t expo-template-blank-typescript --name Blog --yarn
  6. Register heading coordinates using Custom Renderers

    master

    To enable the Scroller to know where headings are located, you must register their layout coordinates. This is achieved by creating custom renderers for h2, h3, and header tags using react-native-render-html's custom renderer system.

    1. Create a HeadingRenderer that calls scroller.registerScrollEntry inside its onLayout handler.
    2. Create a HeaderRenderer that calls scroller.setOffset inside its onLayout handler to account for the header's height.
    3. Pass these renderers to the RenderHTMLConfigProvider via the renderers prop.

    Note: Since these tags have a block content model, they are rendered as View components, allowing you to pass onLayout via the viewProps prop to the TDefaultRenderer.

    const HeadingRenderer: CustomBlockRenderer = function HeaderRenderer({
      TDefaultRenderer,
      ...props
    }) {
      const scroller = useScroller();
      const onLayout = useCallback(
        (e: LayoutChangeEvent) => {
          scroller.registerScrollEntry(textContent(props.tnode.domNode!), e);
        },
        [scroller]
      );
      return <TDefaultRenderer {...props} viewProps={{ onLayout }} />;
    };
    
    const HeaderRenderer: CustomBlockRenderer = function HeaderRenderer({
      TDefaultRenderer,
      ...props
    }) {
      const scroller = useScroller();
      const onLayout = useCallback(
        (e: LayoutChangeEvent) => {
          scroller.setOffset(e.nativeEvent.layout.y + e.nativeEvent.layout.height);
        },
        [scroller]
      );
      return <TDefaultRenderer {...props} viewProps={{ onLayout }} />;
    };
    
    const renderers: CustomTagRendererRecord = {
      h2: HeadingRenderer,
      h3: HeadingRenderer,
      header: HeaderRenderer,
    };
  7. Share the Scroller instance via React Context

    master

    To make the Scroller instance accessible throughout your component tree (e.g., to allow a Table of Contents component to trigger scrolls in a separate Article Body component), use a React Context provider.

    1. Create a ScrollerProvider and a useScroller hook.
    2. Wrap your screen or application with ScrollerProvider and pass the Scroller instance.
    3. Consume the instance using useScroller() in child components.
    import React, { createContext, PropsWithChildren, useContext } from "react";
    import Scroller from './Scroller';
    
    const scrollerContext = createContext<Scroller>(null as any);
    
    export function useScroller(): Scroller {
      return useContext(scrollerContext);
    }
    
    export function ScrollerProvider({
      children,
      scroller
    }: PropsWithChildren<{ scroller: Scroller }>) {
      return (
        <scrollerContext.Provider value={scroller}>
          {children}
        </scrollerContext.Provider>
      );
    }