How Taro Dynamically Inserts Nodes: Inside React‑Based Mini‑Program Rendering

This article explains how Taro implements dynamic node insertion on mini‑program platforms by leveraging @tarojs/react and @tarojs/runtime, mimicking web‑style DOM operations through a custom Document abstraction, detailing the underlying classes, update mechanisms, and code examples for creating and destroying toast notifications.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How Taro Dynamically Inserts Nodes: Inside React‑Based Mini‑Program Rendering

What is Dynamic Node Insertion

In the web we can add a Toast component via an API by creating a DOM element and rendering a React component into it:

const Toast = (props) => {
  return <div>{props.msg}</div>
}
const showToast = (options) => {
  const div = document.createElement('div');
  div.setAttribute('id', 'toast');
  document.body.appendChild(div);
  ReactDOM.render(React.createElement(Toast, options), div);
}
const hideToast = () => {
  if (!document.getElementById("toast")) return;
  document.getElementById("toast").remove()
}

Web relies on the Document object to manipulate the DOM, but mini‑programs do not expose a Document API, so we need an alternative.

Dynamic Node Insertion in Taro

import React from 'react';
import Taro from '@tarojs/taro';
import { render, unmountComponentAtNode } from '@tarojs/react';
import { document, TaroRootElement } from '@tarojs/runtime';
import { View } from '@tarojs/components';

const Demo = ({ msg }) => {
  return <View style={{ position: 'fixed', top: 0 }}>{msg}</View>;
};

export const createNotification = (msg: string) => {
  const view = document.createElement('view');
  const currentPages = Taro.getCurrentPages();
  const currentPage = currentPages[currentPages.length - 1]; // get current page object
  const path = currentPage.$taroPath;
  const pageElement = document.getElementById<TaroRootElement>(path);
  render(<Demo msg={msg} />, view);
  pageElement?.appendChild(view);
};

export const destroyNotification = (node) => {
  const currentPages = Taro.getCurrentPages();
  const currentPage = currentPages[currentPages.length - 1];
  const path = currentPage.$taroPath;
  const pageElement = document.getElementById<TaroRootElement>(path);
  unmountComponentAtNode(node);
  pageElement?.remove(node);
};

Why Does This Work?

Reference

When using React we import react and react-dom. The react-dom package depends on react-reconciler, which implements the actual DOM operations.

In short, react defines the API, react-dom implements DOM updates.

react‑dom in Taro

Taro uses @tarojs/react, which also depends on react-reconciler, to operate the mini‑program DOM via @tarojs/runtime.

Source Code Overview

Below is simplified pseudo‑code of the core implementation.

TaroDocument

export class TaroDocument extends TaroElement {
  // …
}

TaroElement

export class TaroElement extends TaroNode {
  // …
}

TaroNode

The appendChild call eventually reaches TaroNode.appendChild, which forwards to insertBefore and ultimately enqueues an update on the root element.

public appendChild(newChild: TaroNode) {
  return this.insertBefore(newChild)
}
public insertBefore<T extends TaroNode>(newChild: T, refChild?: TaroNode | null, isReplace?: boolean): T {
  // … serialization logic …
  if (this._root) {
    if (!refChild) {
      // appendChild
      const isOnlyChild = this.childNodes.length === 1
      if (isOnlyChild) {
        this.updateChildNodes() // update node
      } else {
        this.enqueueUpdate({
          path: newChild._path,
          value: this.hydrate(newChild)
        })
      }
    } else if (isReplace) {
      // replaceChild
      this.enqueueUpdate({
        path: newChild._path,
        value: this.hydrate(newChild)
      })
    } else {
      // insertBefore
      this.updateChildNodes()
    }
  }
}
private updateChildNodes(isClean?: boolean) {
  const cleanChildNodes = () => []
  const rerenderChildNodes = () => {
    const childNodes = this.childNodes.filter(node => !isComment(node))
    return childNodes.map(hydrate)
  }
  this.enqueueUpdate({
    path: `${this._path}.${CHILDNODES}`,
    value: isClean ? cleanChildNodes : rerenderChildNodes
  })
}
public enqueueUpdate(payload: UpdatePayload) {
  this._root?.enqueueUpdate(payload)
}
public get _root(): TaroRootElement | null {
  return this.parentNode?._root || null
}

TaroRootElement

Updates are propagated through enqueueUpdateperformUpdate, which ultimately calls the mini‑program ctx.setData (similar to setState on a page).

public enqueueUpdate(payload: UpdatePayload) {
  this.updatePayloads.push(payload)
  if (!this.pendingUpdate && this.ctx) {
    this.performUpdate()
  }
}
public performUpdate(initRender = false, prerender?: Func) {
  this.pendingUpdate = true
  const ctx = this.ctx!
  // … custom wrapper setData …
  if (isNeedNormalUpdate) {
    ctx.setData(normalUpdate, cb)
  }
}

createPageConfig

The page root element is obtained via document.getElementById<TaroRootElement>($taroPath). The root’s ctx points to the page instance, so ctx.setState maps to the page’s this.setState.

export function createPageConfig(component, pageName?, data?, pageConfig?) {
  const id = pageName ?? `taro_page_${pageId()}`
  const config: PageInstance = {
    [ONLOAD](this: MpInstance, options: Readonly<Record<string, unknown>>, cb?: Func) {
      // …
      const $taroPath = this.$taroPath = getPath(id, uniqueOptions)
      // …
      const mount = () => {
        Current.app!.mount!(component, $taroPath, () => {
          const pageElement = env.document.getElementById<TaroRootElement>($taroPath)
          // …
          if (process.env.TARO_ENV !== 'h5') {
            pageElement.ctx = this
            pageElement.performUpdate(true, cb)
          }
        })
      }
      // …
    },
    // …
  }
  // …
}

Summary

The compiled mini‑program output shows that each page’s index.wxml is generated from the same template, and the rendering logic ultimately updates the mini‑program data via setData, achieving DOM‑like behavior through @tarojs/react and @tarojs/runtime.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontenddom manipulationmini-programdynamic node insertion
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.