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, VirtualElementParent,
VirtualElementChildren, VirtualElementChildren,
VirtualElementReal, VirtualElementReal,
VirtualElementFragment,
} from './teact'; } from './teact';
import { import {
hasElementChanged, hasElementChanged,
@ -16,6 +17,7 @@ import {
mountComponent, mountComponent,
renderComponent, renderComponent,
unmountComponent, unmountComponent,
isFragmentElement,
} from './teact'; } from './teact';
import generateIdFor from '../../util/generateIdFor'; import generateIdFor from '../../util/generateIdFor';
import { DEBUG } from '../../config'; import { DEBUG } from '../../config';
@ -80,6 +82,9 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
const isNewComponent = $new && isComponentElement($new); const isNewComponent = $new && isComponentElement($new);
const $newAsReal = $new as VirtualElementReal; const $newAsReal = $new as VirtualElementReal;
const isCurrentFragment = $current && !isCurrentComponent && isFragmentElement($current);
const isNewFragment = $new && !isNewComponent && isFragmentElement($new);
if ( if (
!skipComponentUpdate !skipComponentUpdate
&& isCurrentComponent && isNewComponent && isCurrentComponent && isNewComponent
@ -105,9 +110,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
if (!$current && $new) { if (!$current && $new) {
if (isNewComponent) { if (isNewComponent || isNewFragment) {
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new; if (isNewComponent) {
mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment }); $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
}
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment });
} else { } else {
const node = createNode($newAsReal); const node = createNode($newAsReal);
$newAsReal.target = node; $newAsReal.target = node;
@ -121,10 +129,13 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
nextSibling = getNextSibling($current); nextSibling = getNextSibling($current);
} }
if (isNewComponent) { if (isNewComponent || isNewFragment) {
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new; if (isNewComponent) {
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
}
remount(parentEl, $current, undefined); remount(parentEl, $current, undefined);
mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment }); mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment });
} else { } else {
const node = createNode($newAsReal); const node = createNode($newAsReal);
$newAsReal.target = node; $newAsReal.target = node;
@ -132,10 +143,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
} else { } else {
const isComponent = isCurrentComponent && isNewComponent; const isComponent = isCurrentComponent && isNewComponent;
if (isComponent) { const isFragment = isCurrentFragment && isNewFragment;
($new as VirtualElementComponent).children = renderChildren(
if (isComponent || isFragment) {
($new as VirtualElementComponent | VirtualElementFragment).children = renderChildren(
$current, $current,
$new as VirtualElementComponent, $new as VirtualElementComponent | VirtualElementFragment,
parentEl, parentEl,
nextSibling, nextSibling,
); );
@ -220,16 +233,20 @@ function setupComponentUpdateListener(
}; };
} }
function mountComponentChildren(parentEl: HTMLElement, $element: VirtualElementComponent, options: { function mountChildren(
nextSibling?: ChildNode; parentEl: HTMLElement,
fragment?: DocumentFragment; $element: VirtualElementComponent | VirtualElementFragment,
}) { options: {
nextSibling?: ChildNode;
fragment?: DocumentFragment;
},
) {
$element.children = $element.children.map(($child, i) => { $element.children = $element.children.map(($child, i) => {
return renderWithVirtual(parentEl, undefined, $child, $element, i, options); 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) => { $element.children.forEach(($child) => {
renderWithVirtual(parentEl, $child, undefined, $element, -1); renderWithVirtual(parentEl, $child, undefined, $element, -1);
}); });
@ -274,9 +291,15 @@ function remount(
node: Node | undefined, node: Node | undefined,
componentNextSibling?: ChildNode, componentNextSibling?: ChildNode,
) { ) {
if (isComponentElement($current)) { const isComponent = isComponentElement($current);
unmountComponent($current.componentInstance); const isFragment = !isComponent && isFragmentElement($current);
unmountComponentChildren(parentEl, $current);
if (isComponent || isFragment) {
if (isComponent) {
unmountComponent($current.componentInstance);
}
unmountChildren(parentEl, $current);
if (node) { if (node) {
insertBefore(parentEl, node, componentNextSibling); insertBefore(parentEl, node, componentNextSibling);
@ -327,7 +350,7 @@ function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, next
} }
function getNextSibling($current: VirtualElement): ChildNode | undefined { function getNextSibling($current: VirtualElement): ChildNode | undefined {
if (isComponentElement($current)) { if (isComponentElement($current) || isFragmentElement($current)) {
const lastChild = $current.children[$current.children.length - 1]; const lastChild = $current.children[$current.children.length - 1];
return getNextSibling(lastChild); return getNextSibling(lastChild);
} }
@ -344,7 +367,7 @@ function renderChildren(
DEBUG_checkKeyUniqueness($new.children); DEBUG_checkKeyUniqueness($new.children);
} }
if ($new.props.teactFastList) { if (('props' in $new) && $new.props.teactFastList) {
return renderFastListChildren($current, $new, currentEl); return renderFastListChildren($current, $new, currentEl);
} }
@ -388,10 +411,16 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
$new.children.map(($newChild) => { $new.children.map(($newChild) => {
const key = 'props' in $newChild && $newChild.props.key; const key = 'props' in $newChild && $newChild.props.key;
// eslint-disable-next-line no-null/no-null if (DEBUG && isParentElement($newChild)) {
if (DEBUG && isParentElement($newChild) && (key === undefined || key === null)) { // eslint-disable-next-line no-null/no-null
// eslint-disable-next-line no-console if (key === undefined || key === null) {
console.warn('Missing `key` in `teactFastList`'); // 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; 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); processControlled(element.tagName, $new.props);
const currentEntries = Object.entries($current.props); const currentEntries = Object.entries($current.props);

View File

@ -19,6 +19,7 @@ export enum VirtualElementTypesEnum {
Text, Text,
Tag, Tag,
Component, Component,
Fragment,
} }
interface VirtualElementEmpty { interface VirtualElementEmpty {
@ -47,6 +48,12 @@ export interface VirtualElementComponent {
children: VirtualElementChildren; children: VirtualElementChildren;
} }
export interface VirtualElementFragment {
type: VirtualElementTypesEnum.Fragment;
target?: Node;
children: VirtualElementChildren;
}
export type StateHookSetter<T> = (newValue: ((current: T) => T) | T) => void; export type StateHookSetter<T> = (newValue: ((current: T) => T) | T) => void;
interface ComponentInstance { interface ComponentInstance {
@ -96,12 +103,14 @@ export type VirtualElement =
VirtualElementEmpty VirtualElementEmpty
| VirtualElementText | VirtualElementText
| VirtualElementTag | VirtualElementTag
| VirtualElementComponent; | VirtualElementComponent
| VirtualElementFragment;
export type VirtualElementParent = export type VirtualElementParent =
VirtualElementTag VirtualElementTag
| VirtualElementComponent; | VirtualElementComponent
| VirtualElementFragment;
export type VirtualElementChildren = VirtualElement[]; export type VirtualElementChildren = VirtualElement[];
export type VirtualElementReal = Exclude<VirtualElement, VirtualElementComponent>; export type VirtualElementReal = Exclude<VirtualElement, VirtualElementComponent | VirtualElementFragment>;
// Compatibility with JSX types // Compatibility with JSX types
export type TeactNode = export type TeactNode =
@ -135,8 +144,12 @@ export function isComponentElement($element: VirtualElement): $element is Virtua
return $element.type === VirtualElementTypesEnum.Component; 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 { export function isParentElement($element: VirtualElement): $element is VirtualElementParent {
return isTagElement($element) || isComponentElement($element); return isTagElement($element) || isComponentElement($element) || isFragmentElement($element);
} }
function createElement( function createElement(
@ -144,21 +157,24 @@ function createElement(
props: Props, props: Props,
...children: any[] ...children: any[]
): VirtualElementParent | VirtualElementChildren { ): VirtualElementParent | VirtualElementChildren {
if (!props) {
props = {};
}
children = children.flat(); children = children.flat();
if (source === Fragment) { if (source === Fragment) {
return children; return buildFragmentElement(children);
} else if (typeof source === 'function') { } else if (typeof source === 'function') {
return createComponentInstance(source, props, children); return createComponentInstance(source, props || {}, children);
} else { } 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 { function createComponentInstance(Component: FC, props: Props, children: any[]): VirtualElementComponent {
let parsedChildren: any | any[] | undefined; let parsedChildren: any | any[] | undefined;
if (children.length === 0) { if (children.length === 0) {