From 6054536ee95cca18117df915f403909b1afeae28 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 22 Nov 2025 12:54:10 +0100 Subject: [PATCH] Update Teact from MyTonWallet --- src/lib/teact/teact-dom.ts | 106 ++++++++++++++++++++----------------- src/lib/teact/teact.ts | 2 +- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 2be69ea4e..c1d971ab1 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -12,7 +12,6 @@ import type { } from './teact'; import { DEBUG } from '../../config'; -import { unique } from '../../util/iteratees'; import { addEventListener, removeAllDelegatedListeners, removeEventListener } from './dom-events'; import { captureImmediateEffects, @@ -59,6 +58,8 @@ const headsByElement = new WeakMap(); const extraClasses = new WeakMap>(); const extraStyles = new WeakMap>(); +const uniqueChildKeysCache = new WeakMap(); + let DEBUG_virtualTreeSize = 1; function render($element: VirtualElement | undefined, parentEl: HTMLElement) { @@ -459,10 +460,6 @@ function renderChildren( forceMoveToEnd = false, namespace?: string, ) { - if (DEBUG) { - DEBUG_checkKeyUniqueness($new.children); - } - if (('props' in $new) && $new.props.teactFastList) { renderFastListChildren($current, $new, currentContext, currentEl); return; @@ -532,52 +529,31 @@ function renderFastListChildren( const currentChildren = $current.children; const newChildren = $new.children; - const newKeys = new Set(); - for (const $newChild of newChildren) { - const key = 'props' in $newChild ? $newChild.props.key : undefined; - - if (DEBUG && isParentElement($newChild)) { - // eslint-disable-next-line no-null/no-null - if (key === undefined || key === null) { - // eslint-disable-next-line no-console - console.warn('Missing `key` in `teactFastList`'); - } + // Clear out duplicated keys to avoid incorrect elements matching + const currentKeysByIndex = getChildKeysByIndex(currentChildren); + const newKeysByIndex = getChildKeysByIndex(newChildren); + const newKeys = new Set(newKeysByIndex); + if (DEBUG) { + for (const $newChild of newChildren) { if ($newChild.type === VirtualType.Fragment) { throw new Error('[Teact] Fragment can not be child of container with `teactFastList`'); } } - - newKeys.add(key); } // Build a collection of old children that also remain in the new list let currentRemainingIndex = 0; - const remainingByKey: Record = {}; + const remainingByKey: Record = {}; for (let i = 0, l = currentChildren.length; i < l; i++) { const $currentChild = currentChildren[i]; - - let key = 'props' in $currentChild ? $currentChild.props.key : undefined; - // eslint-disable-next-line no-null/no-null - const isKeyPresent = key !== undefined && key !== null; + const key = currentKeysByIndex[i]; // First we process removed children - if (isKeyPresent && !newKeys.has(key)) { + if (!newKeys.has(key)) { renderWithVirtual(currentEl, $currentChild, undefined, $new, currentContext, -1); continue; - } else if (!isKeyPresent) { - const $newChild = newChildren[i]; - const newChildKey = ($newChild && 'props' in $newChild) ? $newChild.props.key : undefined; - // If a non-key element remains at the same index we preserve it with a virtual `key` - if ($newChild && !newChildKey) { - key = `${INDEX_KEY_PREFIX}${i}`; - // Otherwise, we just remove it - } else { - renderWithVirtual(currentEl, $currentChild, undefined, $new, currentContext, -1); - - continue; - } } // Then we build up info about remaining children @@ -595,7 +571,7 @@ function renderFastListChildren( for (let i = 0, l = newChildren.length; i < l; i++) { const $newChild = newChildren[i]; - const key = 'props' in $newChild ? $newChild.props.key : `${INDEX_KEY_PREFIX}${i}`; + const key = newKeysByIndex[i]; const currentChildInfo = remainingByKey[key]; if (!currentChildInfo) { @@ -932,23 +908,57 @@ function DEBUG_addToVirtualTreeSize($current: VirtualElementParent | VirtualDomH }); } -function DEBUG_checkKeyUniqueness(children: VirtualElementChildren) { - const firstChild = children[0]; - if (firstChild && 'props' in firstChild && firstChild.props.key !== undefined) { - const keys = children.reduce((acc: any[], child) => { - if ('props' in child && child.props.key) { - acc.push(child.props.key); +/** Returns unique and not missing key for each child */ +function getChildKeysByIndex(children: VirtualElementChildren) { + // The caching makes sense, because each children list is handled at least twice (as the new and the current children) + let uniqueKeysByIndex = uniqueChildKeysCache.get(children); + if (uniqueKeysByIndex) return uniqueKeysByIndex; + + const seenKeys = new Set(); + const DEBUG_duplicatedKeys = new Set(); + + uniqueKeysByIndex = children.map(($child, index) => { + let key = getElementKey($child); + + if (isNullable(key)) { + if (DEBUG && isParentElement($child)) { + // eslint-disable-next-line no-console + console.warn('Missing `key` in `teactFastList`'); } - return acc; - }, []); + key = `${INDEX_KEY_PREFIX}${index}`; + } else { + if (seenKeys.has(key)) { + if (DEBUG) { + DEBUG_duplicatedKeys.add(key); + } - if (keys.length !== unique(keys).length) { - // eslint-disable-next-line no-console - console.warn('[Teact] Duplicated keys:', keys.filter((e, i, a) => a.indexOf(e) !== i), children); - throw new Error('[Teact] Children keys are not unique'); + key = `${INDEX_KEY_PREFIX}${index}`; + } else { + seenKeys.add(key); + } } + + return key; + }); + + if (DEBUG && DEBUG_duplicatedKeys.size) { + // eslint-disable-next-line no-console + console.warn('[Teact] Duplicated keys:', [...DEBUG_duplicatedKeys], children); + throw new Error('[Teact] Children keys are not unique'); } + + uniqueChildKeysCache.set(children, uniqueKeysByIndex); + return uniqueKeysByIndex; +} + +function getElementKey($element: VirtualElement) { + return 'props' in $element ? $element.props.key : undefined; +} + +function isNullable(value: unknown): value is undefined | null { + // eslint-disable-next-line no-null/no-null + return value === undefined || value === null; } const TeactDOM = { render }; diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index c16fa09fa..a64d89d78 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -1,4 +1,3 @@ -/* eslint-disable react-x/no-unnecessary-use-prefix */ import type { ReactElement } from 'react'; import { DEBUG, DEBUG_MORE } from '../../config'; @@ -137,6 +136,7 @@ export type TeactNode = | string | number | boolean + | undefined | TeactNode[]; type Effect = () => (NoneToVoidFunction | void);