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.
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 enqueueUpdate → performUpdate, 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
