Teact: Allow reusing JSX from variables (#6686)

This commit is contained in:
zubiden 2026-02-22 23:43:30 +01:00 committed by Alexander Zinchuk
parent be036138c3
commit 32f98bc0a0
5 changed files with 103 additions and 12 deletions

View File

@ -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 (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<span>
Count:
{' '}
{count}
</span>
</div>
);
};
const STATIC_COUNTER = <Counter />;
export function App() {
const [withFirstReuse, setWithFirstReuse] = useState(true);
return (
<div>
{withFirstReuse && STATIC_COUNTER}
<Counter />
{STATIC_COUNTER}
<button onClick={() => setWithFirstReuse((current) => !current)}>Toggle first reuse</button>
</div>
);
}
export default App;

View File

@ -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 {

View File

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

View File

@ -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<T extends VirtualElement | undefined>(
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;

View File

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