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:
<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
Documentobject 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
reactand
react-dom. The
react-dompackage depends on
react-reconciler, which implements the actual DOM operations.
In short,
reactdefines the API,
react-domimplements 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
appendChildcall eventually reaches
TaroNode.appendChild, which forwards to
insertBeforeand 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
setStateon 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
ctxpoints to the page instance, so
ctx.setStatemaps 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.wxmlis generated from the same template, and the rendering logic ultimately updates the mini‑program data via
setData, achieving DOM‑like behavior through
@tarojs/reactand
@tarojs/runtime.
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.