Teact: Implement createContext and useContextSignal (#4619)

This commit is contained in:
zubiden 2024-06-18 16:30:28 +02:00 committed by Alexander Zinchuk
parent 9d2a928968
commit a4bfdad768
23 changed files with 225 additions and 72 deletions

View File

@ -4,9 +4,9 @@ import React, { memo, useMemo } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import useSelectorSignal from '../../hooks/data/useSelectorSignal';
import useDerivedState from '../../hooks/useDerivedState'; import useDerivedState from '../../hooks/useDerivedState';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useSelectorSignal from '../../hooks/useSelectorSignal';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ListItem from '../ui/ListItem'; import ListItem from '../ui/ListItem';

View File

@ -1,6 +1,6 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useMemo, useRef, useState, memo, useEffect, useMemo, useRef, useSignal, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
@ -118,7 +118,6 @@ import usePrevious from '../../hooks/usePrevious';
import useSchedule from '../../hooks/useSchedule'; import useSchedule from '../../hooks/useSchedule';
import useSendMessageAction from '../../hooks/useSendMessageAction'; import useSendMessageAction from '../../hooks/useSendMessageAction';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useSignal from '../../hooks/useSignal';
import { useStateRef } from '../../hooks/useStateRef'; import { useStateRef } from '../../hooks/useStateRef';
import useSyncEffect from '../../hooks/useSyncEffect'; import useSyncEffect from '../../hooks/useSyncEffect';
import useAttachmentModal from '../middle/composer/hooks/useAttachmentModal'; import useAttachmentModal from '../middle/composer/hooks/useAttachmentModal';

View File

@ -12,13 +12,13 @@ import {
} from '../../../util/dates/workHours'; } from '../../../util/dates/workHours';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import useSelectorSignal from '../../../hooks/data/useSelectorSignal';
import useInterval from '../../../hooks/schedulers/useInterval'; import useInterval from '../../../hooks/schedulers/useInterval';
import useDerivedState from '../../../hooks/useDerivedState'; import useDerivedState from '../../../hooks/useDerivedState';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useForceUpdate from '../../../hooks/useForceUpdate'; import useForceUpdate from '../../../hooks/useForceUpdate';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useSelectorSignal from '../../../hooks/useSelectorSignal';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition'; import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition';

View File

@ -50,13 +50,13 @@ import buildClassName from '../../../util/buildClassName';
import { createLocationHash } from '../../../util/routing'; import { createLocationHash } from '../../../util/routing';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment'; import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment';
import useSelectorSignal from '../../../hooks/data/useSelectorSignal';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useChatContextActions from '../../../hooks/useChatContextActions'; import useChatContextActions from '../../../hooks/useChatContextActions';
import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useSelectorSignal from '../../../hooks/useSelectorSignal';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useChatListEntry from './hooks/useChatListEntry'; import useChatListEntry from './hooks/useChatListEntry';

View File

@ -1,6 +1,6 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useLayoutEffect, useRef, useState, memo, useEffect, useLayoutEffect, useRef, useSignal, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { MediaViewerOrigin, ThreadId } from '../../types'; import type { MediaViewerOrigin, ThreadId } from '../../types';
@ -24,7 +24,6 @@ import useDerivedState from '../../hooks/useDerivedState';
import useHistoryBack from '../../hooks/useHistoryBack'; import useHistoryBack from '../../hooks/useHistoryBack';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useSignal from '../../hooks/useSignal';
import { useSignalRef } from '../../hooks/useSignalRef'; import { useSignalRef } from '../../hooks/useSignalRef';
import { useFullscreenStatus } from '../../hooks/window/useFullscreen'; import { useFullscreenStatus } from '../../hooks/window/useFullscreen';
import useWindowSize from '../../hooks/window/useWindowSize'; import useWindowSize from '../../hooks/window/useWindowSize';

View File

@ -1,7 +1,7 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useLayoutEffect, memo, useEffect, useLayoutEffect,
useMemo, useRef, useState, useMemo, useRef, useSignal, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { ApiDimensions } from '../../api/types'; import type { ApiDimensions } from '../../api/types';
@ -18,7 +18,6 @@ import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { useThrottledSignal } from '../../hooks/useAsyncResolvers'; import { useThrottledSignal } from '../../hooks/useAsyncResolvers';
import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal'; import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useSignal from '../../hooks/useSignal';
import useVideoWaitingSignal from './hooks/useVideoWaitingSignal'; import useVideoWaitingSignal from './hooks/useVideoWaitingSignal';
import ShowTransition from '../ui/ShowTransition'; import ShowTransition from '../ui/ShowTransition';

View File

@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useLayoutEffect, memo, useEffect, useLayoutEffect,
useMemo, useMemo,
useSignal,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { ApiDimensions } from '../../api/types'; import type { ApiDimensions } from '../../api/types';
@ -18,7 +19,6 @@ import useDerivedState from '../../hooks/useDerivedState';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useSignal from '../../hooks/useSignal';
import useControlsSignal from './hooks/useControlsSignal'; import useControlsSignal from './hooks/useControlsSignal';
import Button from '../ui/Button'; import Button from '../ui/Button';

View File

@ -1,9 +1,8 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { useEffect, useRef } from '../../../lib/teact/teact'; import { useEffect, useRef, useSignal } from '../../../lib/teact/teact';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useResizeObserver from '../../../hooks/useResizeObserver'; import useResizeObserver from '../../../hooks/useResizeObserver';
import useSignal from '../../../hooks/useSignal';
export default function useContainerHeight(containerRef: RefObject<HTMLDivElement>, isComposerVisible: boolean) { export default function useContainerHeight(containerRef: RefObject<HTMLDivElement>, isComposerVisible: boolean) {
const [getContainerHeight, setContainerHeight] = useSignal<number | undefined>(); const [getContainerHeight, setContainerHeight] = useSignal<number | undefined>();

View File

@ -1,4 +1,4 @@
import { useEffect, useRef } from '../../../lib/teact/teact'; import { useEffect, useRef, useSignal } from '../../../lib/teact/teact';
import { getGlobal } from '../../../global'; import { getGlobal } from '../../../global';
import type { ThreadId } from '../../../types'; import type { ThreadId } from '../../../types';
@ -13,7 +13,6 @@ import { unique } from '../../../util/iteratees';
import { clamp } from '../../../util/math'; import { clamp } from '../../../util/math';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useSignal from '../../../hooks/useSignal';
type PinnedIntersectionChangedParams = { type PinnedIntersectionChangedParams = {
viewportPinnedIdsToAdd?: number[]; viewportPinnedIdsToAdd?: number[];

View File

@ -3,6 +3,7 @@ import React, {
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
useSignal,
useState, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
@ -28,7 +29,6 @@ import useMediaTransition from '../../../hooks/useMediaTransition';
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress'; import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useSignal from '../../../hooks/useSignal';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import Icon from '../../common/icons/Icon'; import Icon from '../../common/icons/Icon';

View File

@ -15,9 +15,9 @@ import buildClassName from '../../../util/buildClassName';
import { formatDateTimeToString } from '../../../util/dates/dateFormat'; import { formatDateTimeToString } from '../../../util/dates/dateFormat';
import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer'; import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer';
import useSelector from '../../../hooks/data/useSelector';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useSelector from '../../../hooks/useSelector';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import StarIcon from '../../common/icons/StarIcon'; import StarIcon from '../../common/icons/StarIcon';

View File

@ -1,5 +1,5 @@
import React, { import React, {
memo, useEffect, useLayoutEffect, useMemo, useRef, useState, memo, useEffect, useLayoutEffect, useMemo, useRef, useSignal, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
@ -33,7 +33,6 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useHistoryBack from '../../hooks/useHistoryBack'; import useHistoryBack from '../../hooks/useHistoryBack';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import useSignal from '../../hooks/useSignal';
import useWindowSize from '../../hooks/window/useWindowSize'; import useWindowSize from '../../hooks/window/useWindowSize';
import useSlideSizes from './hooks/useSlideSizes'; import useSlideSizes from './hooks/useSlideSizes';

View File

@ -0,0 +1,79 @@
import React, {
createContext, memo, useState,
} from '../../lib/teact/teact';
import useContext from '../../hooks/data/useContext';
const TestingContext = createContext('default value');
const ContextConsumer = ({ children, debugKey }: { children?: any; debugKey?: string }) => {
const value = useContext(TestingContext);
if (debugKey) {
// eslint-disable-next-line no-console
console.log(`ContextConsumer ${debugKey}`, value);
}
return (
<div style="color: red">
{`Current context value: ${value}`}
{children}
</div>
);
};
const MemoizedWrapper = memo(({ children } : { children: any }) => {
return <div style="background-color: aquamarine">{children}</div>;
});
const ContextSwapper = ({ value, children } : { value: string; children: any }) => {
return (
<div style="border: 1px solid red">
Swapped {value}
{children}
</div>
);
};
const TestContext = () => {
const [value, setValue] = useState(Math.random().toString());
const [isSwapping, setIsSwapping] = useState(false);
const Wrapper = isSwapping ? ContextSwapper : TestingContext.Provider;
return (
<div>
<button onClick={() => setValue(Math.random().toString())}>Change context value</button>
<button onClick={() => setIsSwapping((prev) => !prev)}>Swap context wrapper</button>
<ContextConsumer />
<Wrapper value={value}>
<div>
<ContextConsumer debugKey="A">
<ContextConsumer debugKey="B">
<ContextConsumer debugKey="C" />
</ContextConsumer>
</ContextConsumer>
</div>
<TestingContext.Provider value={`sibling ${value}`}>
<ContextConsumer />
</TestingContext.Provider>
<MemoizedWrapper>
<ContextConsumer />
</MemoizedWrapper>
<>
<ContextConsumer />
<ContextConsumer />
</>
<TestingContext.Provider value={`fastlist ${value}`}>
<div style="background-color: antiquewhite" teactFastList>
<ContextConsumer key="first" />
{!isSwapping && <div key="second">Fast list item</div>}
<ContextConsumer key="third" />
</div>
</TestingContext.Provider>
<TestingContext.Provider value="overriden value">
<ContextConsumer />
</TestingContext.Provider>
</Wrapper>
</div>
);
};
export default TestContext;

View File

@ -0,0 +1,8 @@
import { type Context, useContextSignal } from '../../lib/teact/teact';
import useDerivedState from '../useDerivedState';
export default function useContext<T>(context: Context<T>) {
const signal = useContextSignal(context);
return useDerivedState(signal);
}

View File

@ -1,6 +1,6 @@
import type { GlobalState } from '../global/types'; import type { GlobalState } from '../../global/types';
import useDerivedState from './useDerivedState'; import useDerivedState from '../useDerivedState';
import useSelectorSignal from './useSelectorSignal'; import useSelectorSignal from './useSelectorSignal';
type Selector<T extends unknown> = (global: GlobalState) => T; type Selector<T extends unknown> = (global: GlobalState) => T;

View File

@ -1,11 +1,11 @@
import { addCallback } from '../lib/teact/teactn'; import { addCallback } from '../../lib/teact/teactn';
import { getGlobal } from '../global'; import { getGlobal } from '../../global';
import type { GlobalState } from '../global/types'; import type { GlobalState } from '../../global/types';
import type { Signal, SignalSetter } from '../util/signals'; import type { Signal, SignalSetter } from '../../util/signals';
import { createSignal } from '../util/signals'; import { createSignal } from '../../util/signals';
import useEffectOnce from './useEffectOnce'; import useEffectOnce from '../useEffectOnce';
/* /*
This hook is a more performant variation of the standard React `useSelector` hook. It allows to: This hook is a more performant variation of the standard React `useSelector` hook. It allows to:

View File

@ -1,6 +1,7 @@
import { useSignal } from '../lib/teact/teact';
import type { Signal } from '../util/signals'; import type { Signal } from '../util/signals';
import useSignal from './useSignal';
import { useSignalEffect } from './useSignalEffect'; import { useSignalEffect } from './useSignalEffect';
import { useStateRef } from './useStateRef'; import { useStateRef } from './useStateRef';
import useSyncEffect from './useSyncEffect'; import useSyncEffect from './useSyncEffect';

View File

@ -1,6 +1,4 @@
import { useEffect } from '../lib/teact/teact'; import { useEffect, useSignal } from '../lib/teact/teact';
import useSignal from './useSignal';
export default function useGetSelectionRange(inputSelector: string) { export default function useGetSelectionRange(inputSelector: string) {
const [getRange, setRange] = useSignal<Range | undefined>(); const [getRange, setRange] = useSignal<Range | undefined>();

View File

@ -4,8 +4,8 @@ import { ApiMediaFormat } from '../api/types';
import { selectIsSynced } from '../global/selectors'; import { selectIsSynced } from '../global/selectors';
import * as mediaLoader from '../util/mediaLoader'; import * as mediaLoader from '../util/mediaLoader';
import useSelector from './data/useSelector';
import useForceUpdate from './useForceUpdate'; import useForceUpdate from './useForceUpdate';
import useSelector from './useSelector';
const useMedia = ( const useMedia = (
mediaHash: string | false | undefined, mediaHash: string | false | undefined,

View File

@ -8,8 +8,8 @@ import { selectIsSynced } from '../global/selectors';
import * as mediaLoader from '../util/mediaLoader'; import * as mediaLoader from '../util/mediaLoader';
import { throttle } from '../util/schedulers'; import { throttle } from '../util/schedulers';
import { IS_PROGRESSIVE_SUPPORTED } from '../util/windowEnvironment'; import { IS_PROGRESSIVE_SUPPORTED } from '../util/windowEnvironment';
import useSelector from './data/useSelector';
import useForceUpdate from './useForceUpdate'; import useForceUpdate from './useForceUpdate';
import useSelector from './useSelector';
import useUniqueId from './useUniqueId'; import useUniqueId from './useUniqueId';
const STREAMING_PROGRESS = 0.75; const STREAMING_PROGRESS = 0.75;

View File

@ -1,9 +0,0 @@
import { useRef } from '../lib/teact/teact';
import { createSignal } from '../util/signals';
export default function useSignal<T>(initial?: T) {
const signalRef = useRef<ReturnType<typeof createSignal<T>>>();
signalRef.current ??= createSignal<T>(initial);
return signalRef.current;
}

View File

@ -1,5 +1,6 @@
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import type { Signal } from '../../util/signals';
import type { import type {
VirtualElement, VirtualElement,
VirtualElementChildren, VirtualElementChildren,
@ -34,6 +35,8 @@ interface SelectionState {
isCaretAtEnd: boolean; isCaretAtEnd: boolean;
} }
type CurrentContext = Record<string, Signal<unknown>>;
type DOMElement = HTMLElement | SVGElement; type DOMElement = HTMLElement | SVGElement;
const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']); const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']);
@ -59,7 +62,7 @@ function render($element: VirtualElement | undefined, parentEl: HTMLElement) {
const runImmediateEffects = captureImmediateEffects(); const runImmediateEffects = captureImmediateEffects();
const $head = headsByElement.get(parentEl)!; const $head = headsByElement.get(parentEl)!;
const $renderedChild = renderWithVirtual(parentEl, $head.children[0], $element, $head, 0); const $renderedChild = renderWithVirtual(parentEl, $head.children[0], $element, $head, {}, 0);
runImmediateEffects?.(); runImmediateEffects?.();
$head.children = $renderedChild ? [$renderedChild] : []; $head.children = $renderedChild ? [$renderedChild] : [];
@ -79,6 +82,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
$current: VirtualElement | undefined, $current: VirtualElement | undefined,
$new: T, $new: T,
$parent: VirtualElementParent | VirtualDomHead, $parent: VirtualElementParent | VirtualDomHead,
currentContext: CurrentContext,
index: number, index: number,
options: { options: {
skipComponentUpdate?: boolean; skipComponentUpdate?: boolean;
@ -116,7 +120,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
&& isNewComponent && isNewComponent
&& ($new as VirtualElementComponent).componentInstance.mountState === MountState.Mounted && ($new as VirtualElementComponent).componentInstance.mountState === MountState.Mounted
) { ) {
setupComponentUpdateListener(parentEl, $new as VirtualElementComponent, $parent, index); setupComponentUpdateListener(parentEl, $new as VirtualElementComponent, $parent, currentContext, index);
} }
if ($current === $new) { if ($current === $new) {
@ -133,10 +137,13 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
if (!$current && $new) { if (!$current && $new) {
if (isNewComponent || isNewFragment) { if (isNewComponent || isNewFragment) {
if (isNewComponent) { if (isNewComponent) {
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as unknown as typeof $new; $new = initComponent(
parentEl, $new as VirtualElementComponent, $parent, currentContext, index,
) as unknown as typeof $new;
currentContext = ($new as VirtualElementComponent).componentInstance.context ?? currentContext;
} }
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, {
nextSibling, fragment, isSvg, nextSibling, fragment, isSvg,
}); });
} else { } else {
@ -150,7 +157,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
parentEl.textContent = $newAsReal.value; parentEl.textContent = $newAsReal.value;
$newAsReal.target = parentEl.firstChild!; $newAsReal.target = parentEl.firstChild!;
} else { } else {
const node = createNode($newAsReal, isSvg); const node = createNode($newAsReal, currentContext, isSvg);
$newAsReal.target = node; $newAsReal.target = node;
insertBefore(fragment || parentEl, node, nextSibling); insertBefore(fragment || parentEl, node, nextSibling);
@ -160,7 +167,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
} }
} else if ($current && !$new) { } else if ($current && !$new) {
remount(parentEl, $current, undefined); remount(parentEl, $current, currentContext, undefined);
} else if ($current && $new) { } else if ($current && $new) {
if (hasElementChanged($current, $new)) { if (hasElementChanged($current, $new)) {
if (!nextSibling) { if (!nextSibling) {
@ -169,17 +176,20 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
if (isNewComponent || isNewFragment) { if (isNewComponent || isNewFragment) {
if (isNewComponent) { if (isNewComponent) {
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as unknown as typeof $new; $new = initComponent(
parentEl, $new as VirtualElementComponent, $parent, currentContext, index,
) as unknown as typeof $new;
currentContext = ($new as VirtualElementComponent).componentInstance.context ?? currentContext;
} }
remount(parentEl, $current, undefined); remount(parentEl, $current, currentContext, undefined);
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, {
nextSibling, fragment, isSvg, nextSibling, fragment, isSvg,
}); });
} else { } else {
const node = createNode($newAsReal, isSvg); const node = createNode($newAsReal, currentContext, isSvg);
$newAsReal.target = node; $newAsReal.target = node;
remount(parentEl, $current, node, nextSibling); remount(parentEl, $current, currentContext, node, nextSibling);
if ($newAsReal.type === VirtualType.Tag) { if ($newAsReal.type === VirtualType.Tag) {
setElementRef($newAsReal, node as DOMElement); setElementRef($newAsReal, node as DOMElement);
@ -193,6 +203,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
renderChildren( renderChildren(
$current, $current,
$new as VirtualElementComponent | VirtualElementFragment, $new as VirtualElementComponent | VirtualElementFragment,
currentContext,
parentEl, parentEl,
nextSibling, nextSibling,
options.forceMoveToEnd, options.forceMoveToEnd,
@ -216,7 +227,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
updateAttributes($current, $newAsTag, currentTarget as DOMElement, isSvg); updateAttributes($current, $newAsTag, currentTarget as DOMElement, isSvg);
renderChildren($current, $newAsTag, currentTarget as DOMElement, undefined, undefined, isSvg); renderChildren($current, $newAsTag, currentContext, currentTarget as DOMElement, undefined, undefined, isSvg);
} }
} }
} }
@ -229,13 +240,16 @@ function initComponent(
parentEl: DOMElement, parentEl: DOMElement,
$element: VirtualElementComponent, $element: VirtualElementComponent,
$parent: VirtualElementParent | VirtualDomHead, $parent: VirtualElementParent | VirtualDomHead,
currentContext: CurrentContext,
index: number, index: number,
) { ) {
const { componentInstance } = $element; const { componentInstance } = $element;
$element.componentInstance.context = currentContext;
if (componentInstance.mountState === MountState.New) { if (componentInstance.mountState === MountState.New) {
$element = mountComponent(componentInstance); $element = mountComponent(componentInstance);
setupComponentUpdateListener(parentEl, $element, $parent, index); setupComponentUpdateListener(parentEl, $element, $parent, currentContext, index);
} }
return $element; return $element;
@ -251,6 +265,7 @@ function setupComponentUpdateListener(
parentEl: DOMElement, parentEl: DOMElement,
$element: VirtualElementComponent, $element: VirtualElementComponent,
$parent: VirtualElementParent | VirtualDomHead, $parent: VirtualElementParent | VirtualDomHead,
currentContext: CurrentContext,
index: number, index: number,
) { ) {
const { componentInstance } = $element; const { componentInstance } = $element;
@ -261,6 +276,7 @@ function setupComponentUpdateListener(
$parent.children[index], $parent.children[index],
componentInstance.$element, componentInstance.$element,
$parent, $parent,
currentContext,
index, index,
{ skipComponentUpdate: true }, { skipComponentUpdate: true },
); );
@ -270,6 +286,7 @@ function setupComponentUpdateListener(
function mountChildren( function mountChildren(
parentEl: DOMElement, parentEl: DOMElement,
$element: VirtualElementComponent | VirtualElementFragment, $element: VirtualElementComponent | VirtualElementFragment,
currentContext: CurrentContext,
options: { options: {
nextSibling?: ChildNode; nextSibling?: ChildNode;
fragment?: DocumentFragment; fragment?: DocumentFragment;
@ -279,20 +296,22 @@ function mountChildren(
const { children } = $element; const { children } = $element;
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
const $child = children[i]; const $child = children[i];
const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $element, i, options); const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $element, currentContext, i, options);
if ($renderedChild !== $child) { if ($renderedChild !== $child) {
children[i] = $renderedChild; children[i] = $renderedChild;
} }
} }
} }
function unmountChildren(parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment) { function unmountChildren(
parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment, currentContext: CurrentContext,
) {
for (const $child of $element.children) { for (const $child of $element.children) {
renderWithVirtual(parentEl, $child, undefined, $element, -1); renderWithVirtual(parentEl, $child, undefined, $element, currentContext, -1);
} }
} }
function createNode($element: VirtualElementReal, isSvg?: true): Node { function createNode($element: VirtualElementReal, currentContext: CurrentContext, isSvg?: true): Node {
if ($element.type === VirtualType.Empty) { if ($element.type === VirtualType.Empty) {
return document.createTextNode(''); return document.createTextNode('');
} }
@ -319,7 +338,7 @@ function createNode($element: VirtualElementReal, isSvg?: true): Node {
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
const $child = children[i]; const $child = children[i];
const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i, { isSvg }); const $renderedChild = renderWithVirtual(element, undefined, $child, $element, currentContext, i, { isSvg });
if ($renderedChild !== $child) { if ($renderedChild !== $child) {
children[i] = $renderedChild; children[i] = $renderedChild;
} }
@ -331,6 +350,7 @@ function createNode($element: VirtualElementReal, isSvg?: true): Node {
function remount( function remount(
parentEl: DOMElement, parentEl: DOMElement,
$current: VirtualElement, $current: VirtualElement,
currentContext: CurrentContext,
node: Node | undefined, node: Node | undefined,
componentNextSibling?: ChildNode, componentNextSibling?: ChildNode,
) { ) {
@ -342,7 +362,7 @@ function remount(
unmountComponent($current.componentInstance); unmountComponent($current.componentInstance);
} }
unmountChildren(parentEl, $current); unmountChildren(parentEl, $current, currentContext);
if (node) { if (node) {
insertBefore(parentEl, node, componentNextSibling); insertBefore(parentEl, node, componentNextSibling);
@ -400,6 +420,7 @@ function getNextSibling($current: VirtualElement): ChildNode | undefined {
function renderChildren( function renderChildren(
$current: VirtualElementParent, $current: VirtualElementParent,
$new: VirtualElementParent, $new: VirtualElementParent,
currentContext: CurrentContext,
currentEl: DOMElement, currentEl: DOMElement,
nextSibling?: ChildNode, nextSibling?: ChildNode,
forceMoveToEnd = false, forceMoveToEnd = false,
@ -410,7 +431,7 @@ function renderChildren(
} }
if (('props' in $new) && $new.props.teactFastList) { if (('props' in $new) && $new.props.teactFastList) {
renderFastListChildren($current, $new, currentEl); renderFastListChildren($current, $new, currentContext, currentEl);
return; return;
} }
@ -433,6 +454,7 @@ function renderChildren(
currentChildren[i], currentChildren[i],
newChildren[i], newChildren[i],
$new, $new,
currentContext,
i, i,
i >= currentChildrenLength ? { fragment, isSvg } : { nextSibling, forceMoveToEnd, isSvg }, i >= currentChildrenLength ? { fragment, isSvg } : { nextSibling, forceMoveToEnd, isSvg },
); );
@ -449,7 +471,9 @@ function renderChildren(
// This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones. // This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones.
// It also allows to selectively move particular preserved nodes within their DOM list. // It also allows to selectively move particular preserved nodes within their DOM list.
function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: DOMElement) { function renderFastListChildren(
$current: VirtualElementParent, $new: VirtualElementParent, currentContext: CurrentContext, currentEl: DOMElement,
) {
const currentChildren = $current.children; const currentChildren = $current.children;
const newChildren = $new.children; const newChildren = $new.children;
@ -484,7 +508,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
// First we process removed children // First we process removed children
if (isKeyPresent && !newKeys.has(key)) { if (isKeyPresent && !newKeys.has(key)) {
renderWithVirtual(currentEl, $currentChild, undefined, $new, -1); renderWithVirtual(currentEl, $currentChild, undefined, $new, currentContext, -1);
continue; continue;
} else if (!isKeyPresent) { } else if (!isKeyPresent) {
@ -495,7 +519,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
key = `${INDEX_KEY_PREFIX}${i}`; key = `${INDEX_KEY_PREFIX}${i}`;
// Otherwise, we just remove it // Otherwise, we just remove it
} else { } else {
renderWithVirtual(currentEl, $currentChild, undefined, $new, -1); renderWithVirtual(currentEl, $currentChild, undefined, $new, currentContext, -1);
continue; continue;
} }
@ -531,7 +555,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
// This prepends new children to the top // This prepends new children to the top
if (fragmentSize) { if (fragmentSize) {
renderFragment(fragmentIndex!, fragmentSize, currentEl, $new); renderFragment(fragmentIndex!, fragmentSize, currentEl, $new, currentContext);
fragmentSize = undefined; fragmentSize = undefined;
fragmentIndex = undefined; fragmentIndex = undefined;
} }
@ -551,7 +575,9 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
const nextSibling = currentEl.childNodes[isMovingDown ? i + 1 : i]; const nextSibling = currentEl.childNodes[isMovingDown ? i + 1 : i];
const options = shouldMoveNode ? (nextSibling ? { nextSibling } : { forceMoveToEnd: true }) : undefined; const options = shouldMoveNode ? (nextSibling ? { nextSibling } : { forceMoveToEnd: true }) : undefined;
const $renderedChild = renderWithVirtual(currentEl, currentChildInfo.$element, $newChild, $new, i, options); const $renderedChild = renderWithVirtual(
currentEl, currentChildInfo.$element, $newChild, $new, currentContext, i, options,
);
if ($renderedChild !== $newChild) { if ($renderedChild !== $newChild) {
newChildren[i] = $renderedChild; newChildren[i] = $renderedChild;
} }
@ -559,18 +585,24 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
// This appends new children to the bottom // This appends new children to the bottom
if (fragmentSize) { if (fragmentSize) {
renderFragment(fragmentIndex!, fragmentSize, currentEl, $new); renderFragment(fragmentIndex!, fragmentSize, currentEl, $new, currentContext);
} }
} }
function renderFragment( function renderFragment(
fragmentIndex: number, fragmentSize: number, parentEl: DOMElement, $parent: VirtualElementParent, fragmentIndex: number,
fragmentSize: number,
parentEl: DOMElement,
$parent: VirtualElementParent,
currentContext: CurrentContext,
) { ) {
const nextSibling = parentEl.childNodes[fragmentIndex]; const nextSibling = parentEl.childNodes[fragmentIndex];
if (fragmentSize === 1) { if (fragmentSize === 1) {
const $child = $parent.children[fragmentIndex]; const $child = $parent.children[fragmentIndex];
const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $parent, fragmentIndex, { nextSibling }); const $renderedChild = renderWithVirtual(
parentEl, undefined, $child, $parent, currentContext, fragmentIndex, { nextSibling },
);
if ($renderedChild !== $child) { if ($renderedChild !== $child) {
$parent.children[fragmentIndex] = $renderedChild; $parent.children[fragmentIndex] = $renderedChild;
} }
@ -582,7 +614,7 @@ function renderFragment(
for (let i = fragmentIndex; i < fragmentIndex + fragmentSize; i++) { for (let i = fragmentIndex; i < fragmentIndex + fragmentSize; i++) {
const $child = $parent.children[i]; const $child = $parent.children[i];
const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $parent, i, { fragment }); const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $parent, currentContext, i, { fragment });
if ($renderedChild !== $child) { if ($renderedChild !== $child) {
$parent.children[i] = $renderedChild; $parent.children[i] = $renderedChild;
} }

View File

@ -6,7 +6,7 @@ import { incrementOverlayCounter } from '../../util/debugOverlay';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
import safeExec from '../../util/safeExec'; import safeExec from '../../util/safeExec';
import { throttleWith } from '../../util/schedulers'; import { throttleWith } from '../../util/schedulers';
import { isSignal } from '../../util/signals'; import { createSignal, isSignal, type Signal } from '../../util/signals';
import { requestMeasure, requestMutation } from '../fasterdom/fasterdom'; import { requestMeasure, requestMutation } from '../fasterdom/fasterdom';
export type Props = AnyLiteral; export type Props = AnyLiteral;
@ -71,6 +71,7 @@ interface ComponentInstance {
props: Props; props: Props;
renderedValue?: any; renderedValue?: any;
mountState: MountState; mountState: MountState;
context?: Record<string, Signal<unknown>>;
hooks?: { hooks?: {
state?: { state?: {
cursor: number; cursor: number;
@ -132,12 +133,20 @@ export type TeactNode =
type Effect = () => (NoneToVoidFunction | void); type Effect = () => (NoneToVoidFunction | void);
type EffectCleanup = NoneToVoidFunction; type EffectCleanup = NoneToVoidFunction;
export type Context<T> = {
defaultValue?: T;
contextId: string;
Provider: FC<{ value: T; children: TeactNode }>;
};
const Fragment = Symbol('Fragment'); const Fragment = Symbol('Fragment');
const DEBUG_RENDER_THRESHOLD = 7; const DEBUG_RENDER_THRESHOLD = 7;
const DEBUG_EFFECT_THRESHOLD = 7; const DEBUG_EFFECT_THRESHOLD = 7;
const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']); const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']);
let contextCounter = 0;
let lastComponentId = 0; let lastComponentId = 0;
let renderingInstance: ComponentInstance; let renderingInstance: ComponentInstance;
@ -176,7 +185,7 @@ function createComponentInstance(Component: FC, props: Props, children: any[]):
} }
const componentInstance: ComponentInstance = { const componentInstance: ComponentInstance = {
id: ++lastComponentId, id: -1,
$element: undefined as unknown as VirtualElementComponent, $element: undefined as unknown as VirtualElementComponent,
Component, Component,
name: Component.name, name: Component.name,
@ -476,6 +485,7 @@ export function hasElementChanged($old: VirtualElement, $new: VirtualElement) {
} }
export function mountComponent(componentInstance: ComponentInstance) { export function mountComponent(componentInstance: ComponentInstance) {
componentInstance.id = ++lastComponentId;
renderComponent(componentInstance); renderComponent(componentInstance);
componentInstance.mountState = MountState.Mounted; componentInstance.mountState = MountState.Mounted;
return componentInstance.$element; return componentInstance.$element;
@ -900,6 +910,42 @@ export function useRef<T>(initial?: T | null) {
return byCursor[cursor]; return byCursor[cursor];
} }
export function createContext<T>(defaultValue?: T): Context<T> {
const contextId = String(contextCounter++);
function TeactContextProvider(props: { value: T; children: TeactNode }) {
const [getValue, setValue] = useSignal(props.value ?? defaultValue);
// Create a new object to avoid mutations in the parent context
renderingInstance.context = { ...renderingInstance.context };
renderingInstance.context[contextId] = getValue;
setValue(props.value);
return props.children;
}
TeactContextProvider.DEBUG_contentComponentName = contextId;
const context = {
defaultValue,
contextId,
Provider: TeactContextProvider,
};
return context;
}
export function useContextSignal<T>(context: Context<T>) {
const [getDefaultValue] = useSignal(context.defaultValue);
return renderingInstance.context?.[context.contextId] || getDefaultValue;
}
export function useSignal<T>(initial?: T) {
const signalRef = useRef<ReturnType<typeof createSignal<T>>>();
signalRef.current ??= createSignal<T>(initial);
return signalRef.current;
}
export function memo<T extends FC_withDebug>(Component: T, debugKey?: string) { export function memo<T extends FC_withDebug>(Component: T, debugKey?: string) {
function TeactMemoWrapper(props: Props) { function TeactMemoWrapper(props: Props) {
return useMemo( return useMemo(
@ -929,6 +975,10 @@ export function DEBUG_resolveComponentName(Component: FC_withDebug) {
return `memo>${DEBUG_contentComponentName}`; return `memo>${DEBUG_contentComponentName}`;
} }
if (name === 'TeactContextProvider') {
return `context>id${DEBUG_contentComponentName}`;
}
return name + (DEBUG_contentComponentName ? `>${DEBUG_contentComponentName}` : ''); return name + (DEBUG_contentComponentName ? `>${DEBUG_contentComponentName}` : '');
} }