) => {
+ onInput?.(e);
+ onChange?.(e);
+
+ if (value !== undefined) {
+ e.currentTarget.value = value;
+ }
+
+ if (checked !== undefined) {
+ e.currentTarget.checked = checked;
+ }
+ };
+}
+
+function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) {
+ if (!CONTROLLABLE_TAGS.includes(element.tagName)) {
+ return;
+ }
+
+ if (props.defaultValue) {
+ setAttribute(element, 'value', props.defaultValue);
+ }
+
+ if (props.defaultChecked) {
+ setAttribute(element, 'checked', props.defaultChecked);
+ }
+}
+
+function updateAttributes($current: VirtualElementParent, $new: VirtualElementParent, element: HTMLElement) {
+ processControlled(element.tagName, $new.props);
+
+ const currentEntries = Object.entries($current.props);
+ const newEntries = Object.entries($new.props);
+
+ currentEntries.forEach(([key, currentValue]) => {
+ const newValue = $new.props[key];
+
+ if (
+ currentValue !== undefined
+ && (
+ newValue === undefined
+ || (currentValue !== newValue && key.startsWith('on'))
+ )
+ ) {
+ removeAttribute(element, key, currentValue);
+ }
+ });
+
+ newEntries.forEach(([key, newValue]) => {
+ const currentValue = $current.props[key];
+
+ if (newValue !== undefined && newValue !== currentValue) {
+ setAttribute(element, key, newValue);
+ }
+ });
+}
+
+function setAttribute(element: HTMLElement, key: string, value: any) {
+ // An optimization attempt
+ if (key === 'className') {
+ element.className = value;
+ // An optimization attempt
+ } else if (key === 'value') {
+ if ((element as HTMLInputElement).value !== value) {
+ (element as HTMLInputElement).value = value;
+ }
+ } else if (key === 'style') {
+ element.style.cssText = value;
+ } else if (key === 'dangerouslySetInnerHTML') {
+ // eslint-disable-next-line no-underscore-dangle
+ element.innerHTML = value.__html;
+ } else if (key.startsWith('on')) {
+ addEventListener(element, key, value, key.endsWith('Capture'));
+ } else if (key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) {
+ element.setAttribute(key, value);
+ } else if (!FILTERED_ATTRIBUTES.has(key)) {
+ (element as any)[MAPPED_ATTRIBUTES[key] || key] = value;
+ }
+}
+
+function removeAttribute(element: HTMLElement, key: string, value: any) {
+ if (key === 'className') {
+ element.className = '';
+ } else if (key === 'value') {
+ (element as HTMLInputElement).value = '';
+ } else if (key === 'style') {
+ element.style.cssText = '';
+ } else if (key === 'dangerouslySetInnerHTML') {
+ element.innerHTML = '';
+ } else if (key.startsWith('on')) {
+ removeEventListener(element, key, value, key.endsWith('Capture'));
+ } else if (key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) {
+ element.removeAttribute(key);
+ } else if (!FILTERED_ATTRIBUTES.has(key)) {
+ delete (element as any)[MAPPED_ATTRIBUTES[key] || key];
+ }
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+function DEBUG_addToVirtualTreeSize($current: VirtualElementParent | VirtualDomHead) {
+ DEBUG_virtualTreeSize += $current.children.length;
+
+ $current.children.forEach(($child) => {
+ if (isParentElement($child)) {
+ DEBUG_addToVirtualTreeSize($child);
+ }
+ });
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+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);
+ }
+
+ return acc;
+ }, []);
+
+ if (keys.length !== unique(keys).length) {
+ throw new Error('[Teact] Children keys are not unique');
+ }
+ }
+}
+
+const TeactDOM = { render };
+export default TeactDOM;
diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts
new file mode 100644
index 00000000..61ffe7f7
--- /dev/null
+++ b/src/lib/teact/teact.ts
@@ -0,0 +1,683 @@
+import type { ReactElement } from 'react';
+import { DEBUG, DEBUG_MORE } from '../../config';
+import {
+ fastRaf, fastRafPrimary, onTickEnd, onTickEndPrimary, throttleWithPrimaryRaf, throttleWithRaf,
+} from '../../util/schedulers';
+import { orderBy } from '../../util/iteratees';
+import { getUnequalProps } from '../../util/arePropsShallowEqual';
+import { handleError } from '../../util/handleError';
+
+export type Props = AnyLiteral;
+export type FC = (props: P) => any;
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export type FC_withDebug = FC & {
+ DEBUG_contentComponentName?: string;
+};
+
+export enum VirtualElementTypesEnum {
+ Empty,
+ Text,
+ Tag,
+ Component,
+}
+
+interface VirtualElementEmpty {
+ type: VirtualElementTypesEnum.Empty;
+ target?: Node;
+}
+
+interface VirtualElementText {
+ type: VirtualElementTypesEnum.Text;
+ target?: Node;
+ value: string;
+}
+
+export interface VirtualElementTag {
+ type: VirtualElementTypesEnum.Tag;
+ target?: Node;
+ tag: string;
+ props: Props;
+ children: VirtualElementChildren;
+}
+
+export interface VirtualElementComponent {
+ type: VirtualElementTypesEnum.Component;
+ componentInstance: ComponentInstance;
+ props: Props;
+ children: VirtualElementChildren;
+}
+
+export type StateHookSetter = (newValue: ((current: T) => T) | T) => void;
+
+interface ComponentInstance {
+ $element: VirtualElementComponent;
+ Component: FC;
+ name: string;
+ props: Props;
+ renderedValue?: any;
+ isMounted: boolean;
+ hooks: {
+ state: {
+ cursor: number;
+ byCursor: {
+ value: any;
+ nextValue: any;
+ setter: StateHookSetter;
+ }[];
+ };
+ effects: {
+ cursor: number;
+ byCursor: {
+ effect: () => void;
+ dependencies?: readonly any[];
+ cleanup?: Function;
+ }[];
+ };
+ memos: {
+ cursor: number;
+ byCursor: {
+ value: any;
+ dependencies: any[];
+ }[];
+ };
+ refs: {
+ cursor: number;
+ byCursor: {
+ current: any;
+ }[];
+ };
+ };
+ prepareForFrame?: () => void;
+ forceUpdate?: () => void;
+ onUpdate?: () => void;
+}
+
+export type VirtualElement =
+ VirtualElementEmpty
+ | VirtualElementText
+ | VirtualElementTag
+ | VirtualElementComponent;
+export type VirtualElementParent =
+ VirtualElementTag
+ | VirtualElementComponent;
+export type VirtualElementChildren = VirtualElement[];
+export type VirtualElementReal = Exclude;
+
+// Compatibility with JSX types
+export type TeactNode =
+ ReactElement
+ | string
+ | number
+ | boolean;
+
+const Fragment = Symbol('Fragment');
+
+const DEBUG_RENDER_THRESHOLD = 7;
+const DEBUG_EFFECT_THRESHOLD = 7;
+const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']);
+
+let renderingInstance: ComponentInstance;
+
+export function isEmptyElement($element: VirtualElement): $element is VirtualElementEmpty {
+ return $element.type === VirtualElementTypesEnum.Empty;
+}
+
+export function isTextElement($element: VirtualElement): $element is VirtualElementText {
+ return $element.type === VirtualElementTypesEnum.Text;
+}
+
+export function isTagElement($element: VirtualElement): $element is VirtualElementTag {
+ return $element.type === VirtualElementTypesEnum.Tag;
+}
+
+export function isComponentElement($element: VirtualElement): $element is VirtualElementComponent {
+ return $element.type === VirtualElementTypesEnum.Component;
+}
+
+export function isParentElement($element: VirtualElement): $element is VirtualElementParent {
+ return isTagElement($element) || isComponentElement($element);
+}
+
+function createElement(
+ source: string | FC | typeof Fragment,
+ props: Props,
+ ...children: any[]
+): VirtualElementParent | VirtualElementChildren {
+ if (!props) {
+ props = {};
+ }
+
+ children = children.flat();
+
+ if (source === Fragment) {
+ return children;
+ } else if (typeof source === 'function') {
+ return createComponentInstance(source, props, children);
+ } else {
+ return buildTagElement(source, props, children);
+ }
+}
+
+function createComponentInstance(Component: FC, props: Props, children: any[]): VirtualElementComponent {
+ let parsedChildren: any | any[] | undefined;
+ if (children.length === 0) {
+ parsedChildren = undefined;
+ } else if (children.length === 1) {
+ [parsedChildren] = children;
+ } else {
+ parsedChildren = children;
+ }
+
+ const componentInstance: ComponentInstance = {
+ $element: {} as VirtualElementComponent,
+ Component,
+ name: Component.name,
+ props: {
+ ...props,
+ ...(parsedChildren && { children: parsedChildren }),
+ },
+ isMounted: false,
+ hooks: {
+ state: {
+ cursor: 0,
+ byCursor: [],
+ },
+ effects: {
+ cursor: 0,
+ byCursor: [],
+ },
+ memos: {
+ cursor: 0,
+ byCursor: [],
+ },
+ refs: {
+ cursor: 0,
+ byCursor: [],
+ },
+ },
+ };
+
+ componentInstance.$element = buildComponentElement(componentInstance);
+
+ return componentInstance.$element;
+}
+
+function buildComponentElement(
+ componentInstance: ComponentInstance,
+ children: VirtualElementChildren = [],
+): VirtualElementComponent {
+ const builtChildren = dropEmptyTail(children).map(buildChildElement);
+
+ return {
+ type: VirtualElementTypesEnum.Component,
+ componentInstance,
+ props: componentInstance.props,
+ children: builtChildren.length ? builtChildren : [buildEmptyElement()],
+ };
+}
+
+function buildTagElement(tag: string, props: Props, children: any[]): VirtualElementTag {
+ return {
+ type: VirtualElementTypesEnum.Tag,
+ tag,
+ props,
+ children: dropEmptyTail(children).map(buildChildElement),
+ };
+}
+
+// We only need placeholders in the middle of collection (to ensure other elements order).
+function dropEmptyTail(children: any[]) {
+ let i = children.length - 1;
+
+ for (; i >= 0; i--) {
+ if (!isEmptyPlaceholder(children[i])) {
+ break;
+ }
+ }
+
+ return i + 1 < children.length ? children.slice(0, i + 1) : children;
+}
+
+function isEmptyPlaceholder(child: any) {
+ // eslint-disable-next-line no-null/no-null
+ return child === false || child === null || child === undefined;
+}
+
+function buildChildElement(child: any): VirtualElement {
+ if (isEmptyPlaceholder(child)) {
+ return buildEmptyElement();
+ } else if (isParentElement(child)) {
+ return child;
+ } else {
+ return buildTextElement(child);
+ }
+}
+
+function buildTextElement(value: any): VirtualElementText {
+ return {
+ type: VirtualElementTypesEnum.Text,
+ value: String(value),
+ };
+}
+
+function buildEmptyElement(): VirtualElementEmpty {
+ return { type: VirtualElementTypesEnum.Empty };
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+const DEBUG_components: AnyLiteral = {};
+
+document.addEventListener('dblclick', () => {
+ // eslint-disable-next-line no-console
+ console.warn('COMPONENTS', orderBy(Object.values(DEBUG_components), 'renderCount', 'desc'));
+});
+
+export function renderComponent(componentInstance: ComponentInstance) {
+ renderingInstance = componentInstance;
+ componentInstance.hooks.state.cursor = 0;
+ componentInstance.hooks.effects.cursor = 0;
+ componentInstance.hooks.memos.cursor = 0;
+ componentInstance.hooks.refs.cursor = 0;
+
+ const { Component, props } = componentInstance;
+ let newRenderedValue;
+
+ try {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ let DEBUG_startAt: number | undefined;
+ if (DEBUG) {
+ const componentName = componentInstance.name;
+ if (!DEBUG_components[componentName]) {
+ DEBUG_components[componentName] = {
+ componentName,
+ renderCount: 0,
+ renderTimes: [],
+ };
+ }
+
+ if (DEBUG_MORE) {
+ if (!DEBUG_SILENT_RENDERS_FOR.has(componentName)) {
+ // eslint-disable-next-line no-console
+ console.log(`[Teact] Render ${componentName}`);
+ }
+ }
+
+ DEBUG_startAt = performance.now();
+ }
+
+ newRenderedValue = Component(props);
+
+ if (DEBUG) {
+ const duration = performance.now() - DEBUG_startAt!;
+ const componentName = componentInstance.name;
+ if (duration > DEBUG_RENDER_THRESHOLD) {
+ // eslint-disable-next-line no-console
+ console.warn(`[Teact] Slow component render: ${componentName}, ${Math.round(duration)} ms`);
+ }
+ DEBUG_components[componentName].renderTimes.push(duration);
+ DEBUG_components[componentName].renderCount++;
+ }
+ } catch (err: any) {
+ handleError(err);
+
+ newRenderedValue = componentInstance.renderedValue;
+ }
+
+ if (componentInstance.isMounted && newRenderedValue === componentInstance.renderedValue) {
+ return componentInstance.$element;
+ }
+
+ componentInstance.renderedValue = newRenderedValue;
+
+ const children = Array.isArray(newRenderedValue) ? newRenderedValue : [newRenderedValue];
+ componentInstance.$element = buildComponentElement(componentInstance, children);
+
+ return componentInstance.$element;
+}
+
+export function hasElementChanged($old: VirtualElement, $new: VirtualElement) {
+ if (typeof $old !== typeof $new) {
+ return true;
+ } else if ($old.type !== $new.type) {
+ return true;
+ } else if (isTextElement($old) && isTextElement($new)) {
+ return $old.value !== $new.value;
+ } else if (isTagElement($old) && isTagElement($new)) {
+ return ($old.tag !== $new.tag) || ($old.props.key !== $new.props.key);
+ } else if (isComponentElement($old) && isComponentElement($new)) {
+ return (
+ $old.componentInstance.Component !== $new.componentInstance.Component
+ ) || (
+ $old.props.key !== $new.props.key
+ );
+ }
+
+ return false;
+}
+
+export function mountComponent(componentInstance: ComponentInstance) {
+ renderComponent(componentInstance);
+ componentInstance.isMounted = true;
+ return componentInstance.$element;
+}
+
+export function unmountComponent(componentInstance: ComponentInstance) {
+ if (!componentInstance.isMounted) {
+ return;
+ }
+
+ componentInstance.hooks.effects.byCursor.forEach((effect) => {
+ if (effect.cleanup) {
+ try {
+ effect.cleanup();
+ } catch (err: any) {
+ handleError(err);
+ } finally {
+ effect.cleanup = undefined;
+ }
+ }
+ });
+
+ componentInstance.isMounted = false;
+
+ helpGc(componentInstance);
+}
+
+// We need to remove all references to DOM objects. We also clean all other references, just in case
+function helpGc(componentInstance: ComponentInstance) {
+ componentInstance.hooks.effects.byCursor.forEach((hook) => {
+ hook.cleanup = undefined;
+ hook.effect = undefined as any;
+ hook.dependencies = undefined;
+ });
+
+ componentInstance.hooks.state.byCursor.forEach((hook) => {
+ hook.value = undefined;
+ hook.nextValue = undefined;
+ hook.setter = undefined as any;
+ });
+
+ componentInstance.hooks.memos.byCursor.forEach((hook) => {
+ hook.value = undefined as any;
+ hook.dependencies = undefined as any;
+ });
+
+ componentInstance.hooks.refs.byCursor.forEach((hook) => {
+ hook.current = undefined as any;
+ });
+
+ componentInstance.hooks = undefined as any;
+ componentInstance.$element = undefined as any;
+ componentInstance.renderedValue = undefined;
+ componentInstance.Component = undefined as any;
+ componentInstance.props = undefined as any;
+ componentInstance.forceUpdate = undefined;
+ componentInstance.onUpdate = undefined;
+}
+
+function prepareComponentForFrame(componentInstance: ComponentInstance) {
+ if (!componentInstance.isMounted) {
+ return;
+ }
+
+ componentInstance.hooks.state.byCursor.forEach((hook) => {
+ hook.value = hook.nextValue;
+ });
+
+ componentInstance.prepareForFrame = throttleWithPrimaryRaf(() => prepareComponentForFrame(componentInstance));
+ componentInstance.forceUpdate = throttleWithRaf(() => forceUpdateComponent(componentInstance));
+}
+
+function forceUpdateComponent(componentInstance: ComponentInstance) {
+ if (!componentInstance.isMounted || !componentInstance.onUpdate) {
+ return;
+ }
+
+ const currentElement = componentInstance.$element;
+
+ renderComponent(componentInstance);
+
+ if (componentInstance.$element !== currentElement) {
+ componentInstance.onUpdate();
+ }
+}
+
+export function useState(initial?: T, debugKey?: string): [T, StateHookSetter] {
+ const { cursor, byCursor } = renderingInstance.hooks.state;
+
+ if (byCursor[cursor] === undefined) {
+ byCursor[cursor] = {
+ value: initial,
+ nextValue: initial,
+ setter: ((componentInstance) => (newValue: ((current: T) => T) | T) => {
+ if (byCursor[cursor].nextValue !== newValue) {
+ byCursor[cursor].nextValue = typeof newValue === 'function'
+ ? (newValue as (current: T) => T)(byCursor[cursor].value)
+ : newValue;
+
+ if (!componentInstance.prepareForFrame || !componentInstance.forceUpdate) {
+ componentInstance.prepareForFrame = throttleWithPrimaryRaf(
+ () => prepareComponentForFrame(componentInstance),
+ );
+ componentInstance.forceUpdate = throttleWithRaf(
+ () => forceUpdateComponent(componentInstance),
+ );
+ }
+
+ componentInstance.prepareForFrame();
+ componentInstance.forceUpdate();
+
+ if (DEBUG_MORE) {
+ if (componentInstance.name !== 'TeactNContainer') {
+ // eslint-disable-next-line no-console
+ console.log(
+ '[Teact.useState]',
+ componentInstance.name,
+ // `componentInstance.Component` may be set to `null` by GC helper
+ componentInstance.Component && (componentInstance.Component as FC_withDebug).DEBUG_contentComponentName
+ ? `> ${(componentInstance.Component as FC_withDebug).DEBUG_contentComponentName}`
+ : '',
+ debugKey
+ ? `State update for ${debugKey}, next value: `
+ : `State update at cursor #${cursor}, next value: `,
+ byCursor[cursor].nextValue,
+ );
+ }
+ }
+ }
+ })(renderingInstance),
+ };
+ }
+
+ renderingInstance.hooks.state.cursor++;
+
+ return [
+ byCursor[cursor].value,
+ byCursor[cursor].setter,
+ ];
+}
+
+function useLayoutEffectBase(
+ schedulerFn: typeof onTickEnd | typeof requestAnimationFrame,
+ primarySchedulerFn: typeof onTickEnd | typeof requestAnimationFrame,
+ effect: () => Function | void,
+ dependencies?: readonly any[],
+ debugKey?: string,
+) {
+ const { cursor, byCursor } = renderingInstance.hooks.effects;
+ const componentInstance = renderingInstance;
+
+ function execCleanup() {
+ if (!componentInstance.isMounted) {
+ return;
+ }
+
+ const { cleanup } = byCursor[cursor];
+ if (!cleanup) {
+ return;
+ }
+
+ try {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ let DEBUG_startAt: number | undefined;
+ if (DEBUG) {
+ DEBUG_startAt = performance.now();
+ }
+
+ cleanup();
+
+ if (DEBUG) {
+ const duration = performance.now() - DEBUG_startAt!;
+ const componentName = componentInstance.name;
+ if (duration > DEBUG_EFFECT_THRESHOLD) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[Teact] Slow cleanup at effect cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`,
+ );
+ }
+ }
+ } catch (err: any) {
+ handleError(err);
+ } finally {
+ byCursor[cursor].cleanup = undefined;
+ }
+ }
+
+ function exec() {
+ if (!componentInstance.isMounted) {
+ return;
+ }
+
+ execCleanup();
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ let DEBUG_startAt: number | undefined;
+ if (DEBUG) {
+ DEBUG_startAt = performance.now();
+ }
+
+ const result = effect();
+ if (typeof result === 'function') {
+ byCursor[cursor].cleanup = result;
+ }
+
+ if (DEBUG) {
+ const duration = performance.now() - DEBUG_startAt!;
+ const componentName = componentInstance.name;
+ if (duration > DEBUG_EFFECT_THRESHOLD) {
+ // eslint-disable-next-line no-console
+ console.warn(`[Teact] Slow effect at cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`);
+ }
+ }
+ }
+
+ if (byCursor[cursor] !== undefined && dependencies && byCursor[cursor].dependencies) {
+ if (dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies![i])) {
+ if (debugKey) {
+ const causedBy = dependencies.reduce((res, newValue, i) => {
+ const prevValue = byCursor[cursor].dependencies![i];
+ if (newValue !== prevValue) {
+ res.push(`${i}: ${prevValue} => ${newValue}`);
+ }
+
+ return res;
+ }, []);
+
+ // eslint-disable-next-line no-console
+ console.log(`[Teact] Effect "${debugKey}" caused by dependencies.`, causedBy.join(', '));
+ }
+
+ primarySchedulerFn(execCleanup);
+ schedulerFn(exec);
+ }
+ } else {
+ if (debugKey) {
+ // eslint-disable-next-line no-console
+ console.log(`[Teact] Effect "${debugKey}" caused by missing dependencies.`);
+ }
+
+ primarySchedulerFn(execCleanup);
+ schedulerFn(exec);
+ }
+
+ byCursor[cursor] = {
+ effect,
+ dependencies,
+ cleanup: byCursor[cursor]?.cleanup,
+ };
+
+ renderingInstance.hooks.effects.cursor++;
+}
+
+export function useEffect(effect: () => Function | void, dependencies?: readonly any[], debugKey?: string) {
+ return useLayoutEffectBase(fastRaf, fastRafPrimary, effect, dependencies, debugKey);
+}
+
+export function useLayoutEffect(effect: () => Function | void, dependencies?: readonly any[], debugKey?: string) {
+ return useLayoutEffectBase(onTickEnd, onTickEndPrimary, effect, dependencies, debugKey);
+}
+
+export function useMemo(resolver: () => T, dependencies: any[], debugKey?: string): T {
+ const { cursor, byCursor } = renderingInstance.hooks.memos;
+ let { value } = byCursor[cursor] || {};
+
+ if (
+ byCursor[cursor] === undefined
+ || dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies[i])
+ ) {
+ if (DEBUG && debugKey) {
+ // eslint-disable-next-line no-console
+ console.log(
+ `[Teact.useMemo] ${renderingInstance.name} (${debugKey}): Update is caused by:`,
+ byCursor[cursor]
+ ? getUnequalProps(dependencies, byCursor[cursor].dependencies).join(', ')
+ : '[first render]',
+ );
+ }
+
+ value = resolver();
+ }
+
+ byCursor[cursor] = {
+ value,
+ dependencies,
+ };
+
+ renderingInstance.hooks.memos.cursor++;
+
+ return value;
+}
+
+export function useCallback(newCallback: F, dependencies: any[], debugKey?: string): F {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ return useMemo(() => newCallback, dependencies, debugKey);
+}
+
+export function useRef(initial: T): { current: T };
+export function useRef