From 32f98bc0a0cb0798d299d8a16c673eafb116c6ce Mon Sep 17 00:00:00 2001
From: zubiden <19638254+zubiden@users.noreply.github.com>
Date: Sun, 22 Feb 2026 23:43:30 +0100
Subject: [PATCH] Teact: Allow reusing JSX from variables (#6686)
---
src/components/test/TestJsxVariableReuse.tsx | 36 +++++++++++++++++
src/lib/lovely-chart/styles/_buttons.scss | 2 +-
src/lib/lovely-chart/styles/_common.scss | 20 +++++-----
src/lib/teact/teact-dom.ts | 15 +++++++
src/lib/teact/teact.ts | 42 +++++++++++++++++++-
5 files changed, 103 insertions(+), 12 deletions(-)
create mode 100644 src/components/test/TestJsxVariableReuse.tsx
diff --git a/src/components/test/TestJsxVariableReuse.tsx b/src/components/test/TestJsxVariableReuse.tsx
new file mode 100644
index 000000000..6042fe75b
--- /dev/null
+++ b/src/components/test/TestJsxVariableReuse.tsx
@@ -0,0 +1,36 @@
+import { useState } from '../../lib/teact/teact';
+
+// 1. Make sure "First line" is rendered even if followed by a component with single text.
+// 2. Make sure it then can be normally removed (target is preserved).
+// 3. Make sure "Last line" is also rendered.
+
+const Counter = () => {
+ const [count, setCount] = useState(0);
+
+ return (
+
+
+
+ Count:
+ {' '}
+ {count}
+
+
+ );
+};
+
+const STATIC_COUNTER = ;
+
+export function App() {
+ const [withFirstReuse, setWithFirstReuse] = useState(true);
+ return (
+
+ {withFirstReuse && STATIC_COUNTER}
+
+ {STATIC_COUNTER}
+
+
+ );
+}
+
+export default App;
diff --git a/src/lib/lovely-chart/styles/_buttons.scss b/src/lib/lovely-chart/styles/_buttons.scss
index 3abae9d46..49d843701 100644
--- a/src/lib/lovely-chart/styles/_buttons.scss
+++ b/src/lib/lovely-chart/styles/_buttons.scss
@@ -15,6 +15,7 @@
text-decoration: none;
background-color: transparent;
+
transition: opacity 150ms ease;
&:hover {
@@ -77,7 +78,6 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -256 1792 1792' version='1.1'%0A%3E%3Cg transform='matrix(1,0,0,-1,7.5932203,1217.0847)' id='g3003'%3E%3Cpath d='m 1671,970 q 0,-40 -28,-68 L 919,178 783,42 Q 755,14 715,14 675,14 647,42 L 511,178 149,540 q -28,28 -28,68 0,40 28,68 l 136,136 q 28,28 68,28 40,0 68,-28 l 294,-295 656,657 q 28,28 68,28 40,0 68,-28 l 136,-136 q 28,-28 28,-68 z' style='fill:white'/%3E%3C/g%3E%3C/svg%3E");
background-size: 100%;
}
-
}
.lovely-chart--button-label {
diff --git a/src/lib/lovely-chart/styles/_common.scss b/src/lib/lovely-chart/styles/_common.scss
index 4dc40b352..3cf3b44c7 100644
--- a/src/lib/lovely-chart/styles/_common.scss
+++ b/src/lib/lovely-chart/styles/_common.scss
@@ -11,6 +11,16 @@
-webkit-user-select: none;
user-select: none;
+ position: relative;
+
+ overflow: hidden;
+
+ font: 300 13px '-apple-system', 'HelveticaNeue', Helvetica, Arial, sans-serif;
+ color: #222222;
+ text-align: left;
+
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+
html.theme-dark & {
--background-color: #242F3E;
--text-color: #ffffff;
@@ -22,16 +32,6 @@
--tooltip-arrow: #D2D5D7;
}
- position: relative;
-
- overflow: hidden;
-
- font: 300 13px '-apple-system', 'HelveticaNeue', Helvetica, Arial, sans-serif;
- color: #222222;
- text-align: left;
-
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-
// &.lovely-chart--state-invisible > * {
// display: none;
// }
diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts
index 36d5bd008..857f2c7cc 100644
--- a/src/lib/teact/teact-dom.ts
+++ b/src/lib/teact/teact-dom.ts
@@ -15,7 +15,9 @@ import { DEBUG } from '../../config';
import { addEventListener, removeAllDelegatedListeners, removeEventListener } from './dom-events';
import {
captureImmediateEffects,
+ cloneElement,
hasElementChanged,
+ isElementUsed,
isParentElement,
mountComponent,
MountState,
@@ -104,6 +106,19 @@ function renderWithVirtual(
const { skipComponentUpdate, fragment } = options;
let { nextSibling, namespace } = options;
+ // Since JSX elements are used as is, clone is needed to allow JSX reuse
+ if ($new && $current !== $new && isElementUsed($new)) {
+ const isSelfUpdate = (
+ $new.type === VirtualType.Component
+ && $current?.type === VirtualType.Component
+ && $new.componentInstance === $current.componentInstance
+ );
+ if (!isSelfUpdate) {
+ // eslint-disable-next-line react-x/no-clone-element
+ $new = cloneElement($new) as T;
+ }
+ }
+
const isCurrentComponent = $current?.type === VirtualType.Component;
const isNewComponent = $new?.type === VirtualType.Component;
const $newAsReal = $new as VirtualElementReal;
diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts
index 0a0146366..520bda745 100644
--- a/src/lib/teact/teact.ts
+++ b/src/lib/teact/teact.ts
@@ -170,6 +170,45 @@ export function isParentElement($element: VirtualElement): $element is VirtualEl
);
}
+function cloneElement($element: VirtualElement): VirtualElement {
+ switch ($element.type) {
+ case VirtualType.Empty:
+ return { type: VirtualType.Empty };
+ case VirtualType.Text:
+ return { type: VirtualType.Text, value: $element.value };
+ case VirtualType.Tag:
+ return {
+ type: VirtualType.Tag,
+ tag: $element.tag,
+ props: $element.props,
+ children: $element.children.map(cloneElement),
+ };
+ case VirtualType.Component: {
+ const { componentInstance } = $element;
+ return createComponentInstance(componentInstance.Component, componentInstance.props, []);
+ }
+ case VirtualType.Fragment:
+ return {
+ type: VirtualType.Fragment,
+ children: $element.children.map(cloneElement),
+ };
+ }
+}
+
+// Check if an element has been used (mounted/rendered) and needs cloning for reuse
+export function isElementUsed($element: VirtualElement): boolean {
+ switch ($element.type) {
+ case VirtualType.Component:
+ return $element.componentInstance.mountState !== MountState.Unmounted;
+ case VirtualType.Fragment:
+ return $element.placeholderTarget !== undefined || $element.children.some(isElementUsed);
+ case VirtualType.Tag:
+ case VirtualType.Text:
+ case VirtualType.Empty:
+ return $element.target !== undefined;
+ }
+}
+
function createElement(
source: string | FC | typeof Fragment,
props: Props,
@@ -1061,7 +1100,8 @@ export function DEBUG_resolveComponentName(Component: FC_withDebug) {
export default {
createElement,
+ cloneElement,
Fragment,
};
-export { createElement, Fragment };
+export { createElement, cloneElement, Fragment };