Frontend Development 10 min read

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:

<code>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()
}
</code>

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

<code>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);
};
</code>

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

<code>export class TaroDocument extends TaroElement {
  // …
}
</code>

TaroElement

<code>export class TaroElement extends TaroNode {
  // …
}
</code>

TaroNode

The

appendChild

call eventually reaches

TaroNode.appendChild

, which forwards to

insertBefore

and ultimately enqueues an update on the root element.

<code>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
}
</code>

TaroRootElement

Updates are propagated through

enqueueUpdate

performUpdate

, which ultimately calls the mini‑program

ctx.setData

(similar to

setState

on a page).

<code>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)
  }
}
</code>

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

.

<code>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)
          }
        })
      }
      // …
    },
    // …
  }
  // …
}
</code>

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

.

FrontendReactMini ProgramTarodom manipulationdynamic 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

login 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.