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 };