Teact: Fix fragment breaking other elements order

This commit is contained in:
Alexander Zinchuk 2022-09-16 18:28:44 +02:00
parent 819280b191
commit 19ac35b676
3 changed files with 115 additions and 35 deletions

View File

@ -0,0 +1,35 @@
import React, { useRef, useState } from '../../lib/teact/teact';
export function App() {
const [trigger, setTrigger] = useState(false);
return (
<div
className="App"
onClick={() => {
setTrigger((current) => !current);
}}
>
<h2>Click to update</h2>
{trigger ? (
<>
<span>fragment</span>
<span>content</span>
</>
) : undefined}
<Child />
</div>
);
}
function Child() {
const idRef = useRef(String(Math.random()).slice(-4));
return (
<div>
This number should never change: {idRef.current}
</div>
);
}
export default App;

View File

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

View File

@ -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<T> = (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<VirtualElement, VirtualElementComponent>;
export type VirtualElementReal = Exclude<VirtualElement, VirtualElementComponent | VirtualElementFragment>;
// 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) {