diff --git a/src/components/test/TestFragment.tsx b/src/components/test/TestFragment.tsx
new file mode 100644
index 000000000..9c73dad08
--- /dev/null
+++ b/src/components/test/TestFragment.tsx
@@ -0,0 +1,35 @@
+import React, { useRef, useState } from '../../lib/teact/teact';
+
+export function App() {
+ const [trigger, setTrigger] = useState(false);
+
+ return (
+
{
+ setTrigger((current) => !current);
+ }}
+ >
+
Click to update
+ {trigger ? (
+ <>
+ fragment
+ content
+ >
+ ) : undefined}
+
+
+ );
+}
+
+function Child() {
+ const idRef = useRef(String(Math.random()).slice(-4));
+
+ return (
+
+ This number should never change: {idRef.current}
+
+ );
+}
+
+export default App;
diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts
index df6c1f6cc..205852a34 100644
--- a/src/lib/teact/teact-dom.ts
+++ b/src/lib/teact/teact-dom.ts
@@ -5,6 +5,7 @@ import type {
VirtualElementParent,
VirtualElementChildren,
VirtualElementReal,
+ VirtualElementFragment,
} from './teact';
import {
hasElementChanged,
@@ -16,6 +17,7 @@ import {
mountComponent,
renderComponent,
unmountComponent,
+ isFragmentElement,
} from './teact';
import generateIdFor from '../../util/generateIdFor';
import { DEBUG } from '../../config';
@@ -80,6 +82,9 @@ function renderWithVirtual(
const isNewComponent = $new && isComponentElement($new);
const $newAsReal = $new as VirtualElementReal;
+ const isCurrentFragment = $current && !isCurrentComponent && isFragmentElement($current);
+ const isNewFragment = $new && !isNewComponent && isFragmentElement($new);
+
if (
!skipComponentUpdate
&& isCurrentComponent && isNewComponent
@@ -105,9 +110,12 @@ function renderWithVirtual(
}
if (!$current && $new) {
- if (isNewComponent) {
- $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
- mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment });
+ if (isNewComponent || isNewFragment) {
+ if (isNewComponent) {
+ $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
+ }
+
+ mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment });
} else {
const node = createNode($newAsReal);
$newAsReal.target = node;
@@ -121,10 +129,13 @@ function renderWithVirtual(
nextSibling = getNextSibling($current);
}
- if (isNewComponent) {
- $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
+ if (isNewComponent || isNewFragment) {
+ if (isNewComponent) {
+ $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
+ }
+
remount(parentEl, $current, undefined);
- mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment });
+ mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment });
} else {
const node = createNode($newAsReal);
$newAsReal.target = node;
@@ -132,10 +143,12 @@ function renderWithVirtual(
}
} else {
const isComponent = isCurrentComponent && isNewComponent;
- if (isComponent) {
- ($new as VirtualElementComponent).children = renderChildren(
+ const isFragment = isCurrentFragment && isNewFragment;
+
+ if (isComponent || isFragment) {
+ ($new as VirtualElementComponent | VirtualElementFragment).children = renderChildren(
$current,
- $new as VirtualElementComponent,
+ $new as VirtualElementComponent | VirtualElementFragment,
parentEl,
nextSibling,
);
@@ -220,16 +233,20 @@ function setupComponentUpdateListener(
};
}
-function mountComponentChildren(parentEl: HTMLElement, $element: VirtualElementComponent, options: {
- nextSibling?: ChildNode;
- fragment?: DocumentFragment;
-}) {
+function mountChildren(
+ parentEl: HTMLElement,
+ $element: VirtualElementComponent | VirtualElementFragment,
+ options: {
+ nextSibling?: ChildNode;
+ fragment?: DocumentFragment;
+ },
+) {
$element.children = $element.children.map(($child, i) => {
return renderWithVirtual(parentEl, undefined, $child, $element, i, options);
});
}
-function unmountComponentChildren(parentEl: HTMLElement, $element: VirtualElementComponent) {
+function unmountChildren(parentEl: HTMLElement, $element: VirtualElementComponent | VirtualElementFragment) {
$element.children.forEach(($child) => {
renderWithVirtual(parentEl, $child, undefined, $element, -1);
});
@@ -274,9 +291,15 @@ function remount(
node: Node | undefined,
componentNextSibling?: ChildNode,
) {
- if (isComponentElement($current)) {
- unmountComponent($current.componentInstance);
- unmountComponentChildren(parentEl, $current);
+ const isComponent = isComponentElement($current);
+ const isFragment = !isComponent && isFragmentElement($current);
+
+ if (isComponent || isFragment) {
+ if (isComponent) {
+ unmountComponent($current.componentInstance);
+ }
+
+ unmountChildren(parentEl, $current);
if (node) {
insertBefore(parentEl, node, componentNextSibling);
@@ -327,7 +350,7 @@ function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, next
}
function getNextSibling($current: VirtualElement): ChildNode | undefined {
- if (isComponentElement($current)) {
+ if (isComponentElement($current) || isFragmentElement($current)) {
const lastChild = $current.children[$current.children.length - 1];
return getNextSibling(lastChild);
}
@@ -344,7 +367,7 @@ function renderChildren(
DEBUG_checkKeyUniqueness($new.children);
}
- if ($new.props.teactFastList) {
+ if (('props' in $new) && $new.props.teactFastList) {
return renderFastListChildren($current, $new, currentEl);
}
@@ -388,10 +411,16 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
$new.children.map(($newChild) => {
const key = 'props' in $newChild && $newChild.props.key;
- // eslint-disable-next-line no-null/no-null
- if (DEBUG && isParentElement($newChild) && (key === undefined || key === null)) {
- // eslint-disable-next-line no-console
- console.warn('Missing `key` in `teactFastList`');
+ 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`');
+ }
+
+ if (isFragmentElement($newChild)) {
+ throw new Error('[Teact] Fragment can not be child of container with `teactFastList`');
+ }
}
return key;
@@ -555,7 +584,7 @@ function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) {
}
}
-function updateAttributes($current: VirtualElementParent, $new: VirtualElementParent, element: HTMLElement) {
+function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: HTMLElement) {
processControlled(element.tagName, $new.props);
const currentEntries = Object.entries($current.props);
diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts
index 60a60e33b..be26e750e 100644
--- a/src/lib/teact/teact.ts
+++ b/src/lib/teact/teact.ts
@@ -19,6 +19,7 @@ export enum VirtualElementTypesEnum {
Text,
Tag,
Component,
+ Fragment,
}
interface VirtualElementEmpty {
@@ -47,6 +48,12 @@ export interface VirtualElementComponent {
children: VirtualElementChildren;
}
+export interface VirtualElementFragment {
+ type: VirtualElementTypesEnum.Fragment;
+ target?: Node;
+ children: VirtualElementChildren;
+}
+
export type StateHookSetter = (newValue: ((current: T) => T) | T) => void;
interface ComponentInstance {
@@ -96,12 +103,14 @@ export type VirtualElement =
VirtualElementEmpty
| VirtualElementText
| VirtualElementTag
- | VirtualElementComponent;
+ | VirtualElementComponent
+ | VirtualElementFragment;
export type VirtualElementParent =
VirtualElementTag
- | VirtualElementComponent;
+ | VirtualElementComponent
+ | VirtualElementFragment;
export type VirtualElementChildren = VirtualElement[];
-export type VirtualElementReal = Exclude;
+export type VirtualElementReal = Exclude;
// Compatibility with JSX types
export type TeactNode =
@@ -135,8 +144,12 @@ export function isComponentElement($element: VirtualElement): $element is Virtua
return $element.type === VirtualElementTypesEnum.Component;
}
+export function isFragmentElement($element: VirtualElement): $element is VirtualElementFragment {
+ return $element.type === VirtualElementTypesEnum.Fragment;
+}
+
export function isParentElement($element: VirtualElement): $element is VirtualElementParent {
- return isTagElement($element) || isComponentElement($element);
+ return isTagElement($element) || isComponentElement($element) || isFragmentElement($element);
}
function createElement(
@@ -144,21 +157,24 @@ function createElement(
props: Props,
...children: any[]
): VirtualElementParent | VirtualElementChildren {
- if (!props) {
- props = {};
- }
-
children = children.flat();
if (source === Fragment) {
- return children;
+ return buildFragmentElement(children);
} else if (typeof source === 'function') {
- return createComponentInstance(source, props, children);
+ return createComponentInstance(source, props || {}, children);
} else {
- return buildTagElement(source, props, children);
+ return buildTagElement(source, props || {}, children);
}
}
+function buildFragmentElement(children: any[]): VirtualElementFragment {
+ return {
+ type: VirtualElementTypesEnum.Fragment,
+ children: dropEmptyTail(children).map(buildChildElement),
+ };
+}
+
function createComponentInstance(Component: FC, props: Props, children: any[]): VirtualElementComponent {
let parsedChildren: any | any[] | undefined;
if (children.length === 0) {