From a4bfdad76864bf8b978179761211d9f31ae43054 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:30:28 +0200 Subject: [PATCH] Teact: Implement `createContext` and `useContextSignal` (#4619) --- src/components/common/AboutAdsModal.tsx | 2 +- src/components/common/Composer.tsx | 3 +- .../common/profile/BusinessHours.tsx | 2 +- src/components/left/main/Chat.tsx | 2 +- .../mediaViewer/MediaViewerSlides.tsx | 3 +- src/components/mediaViewer/SeekLine.tsx | 3 +- .../mediaViewer/VideoPlayerControls.tsx | 2 +- .../middle/hooks/useContainerHeight.ts | 3 +- .../middle/hooks/usePinnedMessage.ts | 3 +- src/components/middle/message/RoundVideo.tsx | 2 +- .../modals/stars/StarsTransactionItem.tsx | 2 +- src/components/story/StorySlides.tsx | 3 +- src/components/test/TestContext.tsx | 79 ++++++++++++++++ src/hooks/data/useContext.ts | 8 ++ src/hooks/{ => data}/useSelector.ts | 4 +- src/hooks/{ => data}/useSelectorSignal.ts | 12 +-- src/hooks/useDerivedSignal.ts | 3 +- src/hooks/useGetSelectionRange.ts | 4 +- src/hooks/useMedia.ts | 2 +- src/hooks/useMediaWithLoadProgress.ts | 2 +- src/hooks/useSignal.ts | 9 -- src/lib/teact/teact-dom.ts | 90 +++++++++++++------ src/lib/teact/teact.ts | 54 ++++++++++- 23 files changed, 225 insertions(+), 72 deletions(-) create mode 100644 src/components/test/TestContext.tsx create mode 100644 src/hooks/data/useContext.ts rename src/hooks/{ => data}/useSelector.ts (73%) rename src/hooks/{ => data}/useSelectorSignal.ts (79%) delete mode 100644 src/hooks/useSignal.ts diff --git a/src/components/common/AboutAdsModal.tsx b/src/components/common/AboutAdsModal.tsx index 555870985..d881d3963 100644 --- a/src/components/common/AboutAdsModal.tsx +++ b/src/components/common/AboutAdsModal.tsx @@ -4,9 +4,9 @@ import React, { memo, useMemo } from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; import renderText from './helpers/renderText'; +import useSelectorSignal from '../../hooks/data/useSelectorSignal'; import useDerivedState from '../../hooks/useDerivedState'; import useOldLang from '../../hooks/useOldLang'; -import useSelectorSignal from '../../hooks/useSelectorSignal'; import Button from '../ui/Button'; import ListItem from '../ui/ListItem'; diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index eb90da7d3..86aa8672c 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -1,6 +1,6 @@ import type { FC } from '../../lib/teact/teact'; import React, { - memo, useEffect, useMemo, useRef, useState, + memo, useEffect, useMemo, useRef, useSignal, useState, } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; @@ -118,7 +118,6 @@ import usePrevious from '../../hooks/usePrevious'; import useSchedule from '../../hooks/useSchedule'; import useSendMessageAction from '../../hooks/useSendMessageAction'; import useShowTransition from '../../hooks/useShowTransition'; -import useSignal from '../../hooks/useSignal'; import { useStateRef } from '../../hooks/useStateRef'; import useSyncEffect from '../../hooks/useSyncEffect'; import useAttachmentModal from '../middle/composer/hooks/useAttachmentModal'; diff --git a/src/components/common/profile/BusinessHours.tsx b/src/components/common/profile/BusinessHours.tsx index 8f4ef6e29..740da3173 100644 --- a/src/components/common/profile/BusinessHours.tsx +++ b/src/components/common/profile/BusinessHours.tsx @@ -12,13 +12,13 @@ import { } from '../../../util/dates/workHours'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; +import useSelectorSignal from '../../../hooks/data/useSelectorSignal'; import useInterval from '../../../hooks/schedulers/useInterval'; import useDerivedState from '../../../hooks/useDerivedState'; import useFlag from '../../../hooks/useFlag'; import useForceUpdate from '../../../hooks/useForceUpdate'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; -import useSelectorSignal from '../../../hooks/useSelectorSignal'; import ListItem from '../../ui/ListItem'; import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition'; diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 2cf7f7d9e..1682eded5 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -50,13 +50,13 @@ import buildClassName from '../../../util/buildClassName'; import { createLocationHash } from '../../../util/routing'; import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment'; +import useSelectorSignal from '../../../hooks/data/useSelectorSignal'; import useAppLayout from '../../../hooks/useAppLayout'; import useChatContextActions from '../../../hooks/useChatContextActions'; import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useFlag from '../../../hooks/useFlag'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import useLastCallback from '../../../hooks/useLastCallback'; -import useSelectorSignal from '../../../hooks/useSelectorSignal'; import useShowTransition from '../../../hooks/useShowTransition'; import useChatListEntry from './hooks/useChatListEntry'; diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx index 03a036ee7..593af4997 100644 --- a/src/components/mediaViewer/MediaViewerSlides.tsx +++ b/src/components/mediaViewer/MediaViewerSlides.tsx @@ -1,6 +1,6 @@ import type { FC } from '../../lib/teact/teact'; import React, { - memo, useEffect, useLayoutEffect, useRef, useState, + memo, useEffect, useLayoutEffect, useRef, useSignal, useState, } from '../../lib/teact/teact'; import type { MediaViewerOrigin, ThreadId } from '../../types'; @@ -24,7 +24,6 @@ import useDerivedState from '../../hooks/useDerivedState'; import useHistoryBack from '../../hooks/useHistoryBack'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; -import useSignal from '../../hooks/useSignal'; import { useSignalRef } from '../../hooks/useSignalRef'; import { useFullscreenStatus } from '../../hooks/window/useFullscreen'; import useWindowSize from '../../hooks/window/useWindowSize'; diff --git a/src/components/mediaViewer/SeekLine.tsx b/src/components/mediaViewer/SeekLine.tsx index 6973635e5..2b234675b 100644 --- a/src/components/mediaViewer/SeekLine.tsx +++ b/src/components/mediaViewer/SeekLine.tsx @@ -1,7 +1,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useLayoutEffect, - useMemo, useRef, useState, + useMemo, useRef, useSignal, useState, } from '../../lib/teact/teact'; import type { ApiDimensions } from '../../api/types'; @@ -18,7 +18,6 @@ import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { useThrottledSignal } from '../../hooks/useAsyncResolvers'; import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal'; import useLastCallback from '../../hooks/useLastCallback'; -import useSignal from '../../hooks/useSignal'; import useVideoWaitingSignal from './hooks/useVideoWaitingSignal'; import ShowTransition from '../ui/ShowTransition'; diff --git a/src/components/mediaViewer/VideoPlayerControls.tsx b/src/components/mediaViewer/VideoPlayerControls.tsx index d5218aac1..02e1c5867 100644 --- a/src/components/mediaViewer/VideoPlayerControls.tsx +++ b/src/components/mediaViewer/VideoPlayerControls.tsx @@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useLayoutEffect, useMemo, + useSignal, } from '../../lib/teact/teact'; import type { ApiDimensions } from '../../api/types'; @@ -18,7 +19,6 @@ import useDerivedState from '../../hooks/useDerivedState'; import useFlag from '../../hooks/useFlag'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; -import useSignal from '../../hooks/useSignal'; import useControlsSignal from './hooks/useControlsSignal'; import Button from '../ui/Button'; diff --git a/src/components/middle/hooks/useContainerHeight.ts b/src/components/middle/hooks/useContainerHeight.ts index cc18a6df5..e8d5575e3 100644 --- a/src/components/middle/hooks/useContainerHeight.ts +++ b/src/components/middle/hooks/useContainerHeight.ts @@ -1,9 +1,8 @@ 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 useResizeObserver from '../../../hooks/useResizeObserver'; -import useSignal from '../../../hooks/useSignal'; export default function useContainerHeight(containerRef: RefObject, isComposerVisible: boolean) { const [getContainerHeight, setContainerHeight] = useSignal(); diff --git a/src/components/middle/hooks/usePinnedMessage.ts b/src/components/middle/hooks/usePinnedMessage.ts index 4c2a9312b..913876ce9 100644 --- a/src/components/middle/hooks/usePinnedMessage.ts +++ b/src/components/middle/hooks/usePinnedMessage.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from '../../../lib/teact/teact'; +import { useEffect, useRef, useSignal } from '../../../lib/teact/teact'; import { getGlobal } from '../../../global'; import type { ThreadId } from '../../../types'; @@ -13,7 +13,6 @@ import { unique } from '../../../util/iteratees'; import { clamp } from '../../../util/math'; import useLastCallback from '../../../hooks/useLastCallback'; -import useSignal from '../../../hooks/useSignal'; type PinnedIntersectionChangedParams = { viewportPinnedIdsToAdd?: number[]; diff --git a/src/components/middle/message/RoundVideo.tsx b/src/components/middle/message/RoundVideo.tsx index 7a5069927..9a40698be 100644 --- a/src/components/middle/message/RoundVideo.tsx +++ b/src/components/middle/message/RoundVideo.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useLayoutEffect, useRef, + useSignal, useState, } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; @@ -28,7 +29,6 @@ import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress'; import usePrevious from '../../../hooks/usePrevious'; import useShowTransition from '../../../hooks/useShowTransition'; -import useSignal from '../../../hooks/useSignal'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import Icon from '../../common/icons/Icon'; diff --git a/src/components/modals/stars/StarsTransactionItem.tsx b/src/components/modals/stars/StarsTransactionItem.tsx index 7c0e91f84..0d58e4628 100644 --- a/src/components/modals/stars/StarsTransactionItem.tsx +++ b/src/components/modals/stars/StarsTransactionItem.tsx @@ -15,9 +15,9 @@ import buildClassName from '../../../util/buildClassName'; import { formatDateTimeToString } from '../../../util/dates/dateFormat'; import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer'; +import useSelector from '../../../hooks/data/useSelector'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; -import useSelector from '../../../hooks/useSelector'; import Avatar from '../../common/Avatar'; import StarIcon from '../../common/icons/StarIcon'; diff --git a/src/components/story/StorySlides.tsx b/src/components/story/StorySlides.tsx index c93e76073..a2c9594d5 100644 --- a/src/components/story/StorySlides.tsx +++ b/src/components/story/StorySlides.tsx @@ -1,5 +1,5 @@ import React, { - memo, useEffect, useLayoutEffect, useMemo, useRef, useState, + memo, useEffect, useLayoutEffect, useMemo, useRef, useSignal, useState, } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; @@ -33,7 +33,6 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useHistoryBack from '../../hooks/useHistoryBack'; import useLastCallback from '../../hooks/useLastCallback'; import usePrevious from '../../hooks/usePrevious'; -import useSignal from '../../hooks/useSignal'; import useWindowSize from '../../hooks/window/useWindowSize'; import useSlideSizes from './hooks/useSlideSizes'; diff --git a/src/components/test/TestContext.tsx b/src/components/test/TestContext.tsx new file mode 100644 index 000000000..fe09130b8 --- /dev/null +++ b/src/components/test/TestContext.tsx @@ -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 ( +
+ {`Current context value: ${value}`} + {children} +
+ ); +}; + +const MemoizedWrapper = memo(({ children } : { children: any }) => { + return
{children}
; +}); + +const ContextSwapper = ({ value, children } : { value: string; children: any }) => { + return ( +
+ Swapped {value} + {children} +
+ ); +}; + +const TestContext = () => { + const [value, setValue] = useState(Math.random().toString()); + const [isSwapping, setIsSwapping] = useState(false); + + const Wrapper = isSwapping ? ContextSwapper : TestingContext.Provider; + return ( +
+ + + + +
+ + + + + +
+ + + + + + + <> + + + + +
+ + {!isSwapping &&
Fast list item
} + +
+
+ + + +
+
+ ); +}; + +export default TestContext; diff --git a/src/hooks/data/useContext.ts b/src/hooks/data/useContext.ts new file mode 100644 index 000000000..2507a48f6 --- /dev/null +++ b/src/hooks/data/useContext.ts @@ -0,0 +1,8 @@ +import { type Context, useContextSignal } from '../../lib/teact/teact'; + +import useDerivedState from '../useDerivedState'; + +export default function useContext(context: Context) { + const signal = useContextSignal(context); + return useDerivedState(signal); +} diff --git a/src/hooks/useSelector.ts b/src/hooks/data/useSelector.ts similarity index 73% rename from src/hooks/useSelector.ts rename to src/hooks/data/useSelector.ts index 4bffa22dd..cca3e96dc 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/data/useSelector.ts @@ -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'; type Selector = (global: GlobalState) => T; diff --git a/src/hooks/useSelectorSignal.ts b/src/hooks/data/useSelectorSignal.ts similarity index 79% rename from src/hooks/useSelectorSignal.ts rename to src/hooks/data/useSelectorSignal.ts index 1f10d0a29..f550a2275 100644 --- a/src/hooks/useSelectorSignal.ts +++ b/src/hooks/data/useSelectorSignal.ts @@ -1,11 +1,11 @@ -import { addCallback } from '../lib/teact/teactn'; -import { getGlobal } from '../global'; +import { addCallback } from '../../lib/teact/teactn'; +import { getGlobal } from '../../global'; -import type { GlobalState } from '../global/types'; -import type { Signal, SignalSetter } from '../util/signals'; +import type { GlobalState } from '../../global/types'; +import type { Signal, SignalSetter } from '../../util/signals'; -import { createSignal } from '../util/signals'; -import useEffectOnce from './useEffectOnce'; +import { createSignal } from '../../util/signals'; +import useEffectOnce from '../useEffectOnce'; /* This hook is a more performant variation of the standard React `useSelector` hook. It allows to: diff --git a/src/hooks/useDerivedSignal.ts b/src/hooks/useDerivedSignal.ts index 5fe38b720..d5f89a20a 100644 --- a/src/hooks/useDerivedSignal.ts +++ b/src/hooks/useDerivedSignal.ts @@ -1,6 +1,7 @@ +import { useSignal } from '../lib/teact/teact'; + import type { Signal } from '../util/signals'; -import useSignal from './useSignal'; import { useSignalEffect } from './useSignalEffect'; import { useStateRef } from './useStateRef'; import useSyncEffect from './useSyncEffect'; diff --git a/src/hooks/useGetSelectionRange.ts b/src/hooks/useGetSelectionRange.ts index bf5968522..bbff6231b 100644 --- a/src/hooks/useGetSelectionRange.ts +++ b/src/hooks/useGetSelectionRange.ts @@ -1,6 +1,4 @@ -import { useEffect } from '../lib/teact/teact'; - -import useSignal from './useSignal'; +import { useEffect, useSignal } from '../lib/teact/teact'; export default function useGetSelectionRange(inputSelector: string) { const [getRange, setRange] = useSignal(); diff --git a/src/hooks/useMedia.ts b/src/hooks/useMedia.ts index 6e3420b4e..26c4a6614 100644 --- a/src/hooks/useMedia.ts +++ b/src/hooks/useMedia.ts @@ -4,8 +4,8 @@ import { ApiMediaFormat } from '../api/types'; import { selectIsSynced } from '../global/selectors'; import * as mediaLoader from '../util/mediaLoader'; +import useSelector from './data/useSelector'; import useForceUpdate from './useForceUpdate'; -import useSelector from './useSelector'; const useMedia = ( mediaHash: string | false | undefined, diff --git a/src/hooks/useMediaWithLoadProgress.ts b/src/hooks/useMediaWithLoadProgress.ts index b228890a7..1162bc27d 100644 --- a/src/hooks/useMediaWithLoadProgress.ts +++ b/src/hooks/useMediaWithLoadProgress.ts @@ -8,8 +8,8 @@ import { selectIsSynced } from '../global/selectors'; import * as mediaLoader from '../util/mediaLoader'; import { throttle } from '../util/schedulers'; import { IS_PROGRESSIVE_SUPPORTED } from '../util/windowEnvironment'; +import useSelector from './data/useSelector'; import useForceUpdate from './useForceUpdate'; -import useSelector from './useSelector'; import useUniqueId from './useUniqueId'; const STREAMING_PROGRESS = 0.75; diff --git a/src/hooks/useSignal.ts b/src/hooks/useSignal.ts deleted file mode 100644 index 096f7a850..000000000 --- a/src/hooks/useSignal.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useRef } from '../lib/teact/teact'; - -import { createSignal } from '../util/signals'; - -export default function useSignal(initial?: T) { - const signalRef = useRef>>(); - signalRef.current ??= createSignal(initial); - return signalRef.current; -} diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 4c9e288c0..95b7c1881 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -1,5 +1,6 @@ import type { ChangeEvent } from 'react'; +import type { Signal } from '../../util/signals'; import type { VirtualElement, VirtualElementChildren, @@ -34,6 +35,8 @@ interface SelectionState { isCaretAtEnd: boolean; } +type CurrentContext = Record>; + type DOMElement = HTMLElement | SVGElement; const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']); @@ -59,7 +62,7 @@ function render($element: VirtualElement | undefined, parentEl: HTMLElement) { const runImmediateEffects = captureImmediateEffects(); 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?.(); $head.children = $renderedChild ? [$renderedChild] : []; @@ -79,6 +82,7 @@ function renderWithVirtual( $current: VirtualElement | undefined, $new: T, $parent: VirtualElementParent | VirtualDomHead, + currentContext: CurrentContext, index: number, options: { skipComponentUpdate?: boolean; @@ -116,7 +120,7 @@ function renderWithVirtual( && isNewComponent && ($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) { @@ -133,10 +137,13 @@ function renderWithVirtual( if (!$current && $new) { if (isNewComponent || isNewFragment) { 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, }); } else { @@ -150,7 +157,7 @@ function renderWithVirtual( parentEl.textContent = $newAsReal.value; $newAsReal.target = parentEl.firstChild!; } else { - const node = createNode($newAsReal, isSvg); + const node = createNode($newAsReal, currentContext, isSvg); $newAsReal.target = node; insertBefore(fragment || parentEl, node, nextSibling); @@ -160,7 +167,7 @@ function renderWithVirtual( } } } else if ($current && !$new) { - remount(parentEl, $current, undefined); + remount(parentEl, $current, currentContext, undefined); } else if ($current && $new) { if (hasElementChanged($current, $new)) { if (!nextSibling) { @@ -169,17 +176,20 @@ function renderWithVirtual( if (isNewComponent || isNewFragment) { 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); - mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { + remount(parentEl, $current, currentContext, undefined); + mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, { nextSibling, fragment, isSvg, }); } else { - const node = createNode($newAsReal, isSvg); + const node = createNode($newAsReal, currentContext, isSvg); $newAsReal.target = node; - remount(parentEl, $current, node, nextSibling); + remount(parentEl, $current, currentContext, node, nextSibling); if ($newAsReal.type === VirtualType.Tag) { setElementRef($newAsReal, node as DOMElement); @@ -193,6 +203,7 @@ function renderWithVirtual( renderChildren( $current, $new as VirtualElementComponent | VirtualElementFragment, + currentContext, parentEl, nextSibling, options.forceMoveToEnd, @@ -216,7 +227,7 @@ function renderWithVirtual( } 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, $element: VirtualElementComponent, $parent: VirtualElementParent | VirtualDomHead, + currentContext: CurrentContext, index: number, ) { const { componentInstance } = $element; + $element.componentInstance.context = currentContext; + if (componentInstance.mountState === MountState.New) { $element = mountComponent(componentInstance); - setupComponentUpdateListener(parentEl, $element, $parent, index); + setupComponentUpdateListener(parentEl, $element, $parent, currentContext, index); } return $element; @@ -251,6 +265,7 @@ function setupComponentUpdateListener( parentEl: DOMElement, $element: VirtualElementComponent, $parent: VirtualElementParent | VirtualDomHead, + currentContext: CurrentContext, index: number, ) { const { componentInstance } = $element; @@ -261,6 +276,7 @@ function setupComponentUpdateListener( $parent.children[index], componentInstance.$element, $parent, + currentContext, index, { skipComponentUpdate: true }, ); @@ -270,6 +286,7 @@ function setupComponentUpdateListener( function mountChildren( parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment, + currentContext: CurrentContext, options: { nextSibling?: ChildNode; fragment?: DocumentFragment; @@ -279,20 +296,22 @@ function mountChildren( const { children } = $element; for (let i = 0, l = children.length; i < l; 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) { 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) { - 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) { return document.createTextNode(''); } @@ -319,7 +338,7 @@ function createNode($element: VirtualElementReal, isSvg?: true): Node { for (let i = 0, l = children.length; i < l; 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) { children[i] = $renderedChild; } @@ -331,6 +350,7 @@ function createNode($element: VirtualElementReal, isSvg?: true): Node { function remount( parentEl: DOMElement, $current: VirtualElement, + currentContext: CurrentContext, node: Node | undefined, componentNextSibling?: ChildNode, ) { @@ -342,7 +362,7 @@ function remount( unmountComponent($current.componentInstance); } - unmountChildren(parentEl, $current); + unmountChildren(parentEl, $current, currentContext); if (node) { insertBefore(parentEl, node, componentNextSibling); @@ -400,6 +420,7 @@ function getNextSibling($current: VirtualElement): ChildNode | undefined { function renderChildren( $current: VirtualElementParent, $new: VirtualElementParent, + currentContext: CurrentContext, currentEl: DOMElement, nextSibling?: ChildNode, forceMoveToEnd = false, @@ -410,7 +431,7 @@ function renderChildren( } if (('props' in $new) && $new.props.teactFastList) { - renderFastListChildren($current, $new, currentEl); + renderFastListChildren($current, $new, currentContext, currentEl); return; } @@ -433,6 +454,7 @@ function renderChildren( currentChildren[i], newChildren[i], $new, + currentContext, i, 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. // 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 newChildren = $new.children; @@ -484,7 +508,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle // First we process removed children if (isKeyPresent && !newKeys.has(key)) { - renderWithVirtual(currentEl, $currentChild, undefined, $new, -1); + renderWithVirtual(currentEl, $currentChild, undefined, $new, currentContext, -1); continue; } else if (!isKeyPresent) { @@ -495,7 +519,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle key = `${INDEX_KEY_PREFIX}${i}`; // Otherwise, we just remove it } else { - renderWithVirtual(currentEl, $currentChild, undefined, $new, -1); + renderWithVirtual(currentEl, $currentChild, undefined, $new, currentContext, -1); continue; } @@ -531,7 +555,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle // This prepends new children to the top if (fragmentSize) { - renderFragment(fragmentIndex!, fragmentSize, currentEl, $new); + renderFragment(fragmentIndex!, fragmentSize, currentEl, $new, currentContext); fragmentSize = undefined; fragmentIndex = undefined; } @@ -551,7 +575,9 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle const nextSibling = currentEl.childNodes[isMovingDown ? i + 1 : i]; 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) { newChildren[i] = $renderedChild; } @@ -559,18 +585,24 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle // This appends new children to the bottom if (fragmentSize) { - renderFragment(fragmentIndex!, fragmentSize, currentEl, $new); + renderFragment(fragmentIndex!, fragmentSize, currentEl, $new, currentContext); } } 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]; if (fragmentSize === 1) { 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) { $parent.children[fragmentIndex] = $renderedChild; } @@ -582,7 +614,7 @@ function renderFragment( for (let i = fragmentIndex; i < fragmentIndex + fragmentSize; 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) { $parent.children[i] = $renderedChild; } diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 57ed281d2..5edacc7eb 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -6,7 +6,7 @@ import { incrementOverlayCounter } from '../../util/debugOverlay'; import { orderBy } from '../../util/iteratees'; import safeExec from '../../util/safeExec'; import { throttleWith } from '../../util/schedulers'; -import { isSignal } from '../../util/signals'; +import { createSignal, isSignal, type Signal } from '../../util/signals'; import { requestMeasure, requestMutation } from '../fasterdom/fasterdom'; export type Props = AnyLiteral; @@ -71,6 +71,7 @@ interface ComponentInstance { props: Props; renderedValue?: any; mountState: MountState; + context?: Record>; hooks?: { state?: { cursor: number; @@ -132,12 +133,20 @@ export type TeactNode = type Effect = () => (NoneToVoidFunction | void); type EffectCleanup = NoneToVoidFunction; +export type Context = { + defaultValue?: T; + contextId: string; + Provider: FC<{ value: T; children: TeactNode }>; +}; + 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 contextCounter = 0; + let lastComponentId = 0; let renderingInstance: ComponentInstance; @@ -176,7 +185,7 @@ function createComponentInstance(Component: FC, props: Props, children: any[]): } const componentInstance: ComponentInstance = { - id: ++lastComponentId, + id: -1, $element: undefined as unknown as VirtualElementComponent, Component, name: Component.name, @@ -476,6 +485,7 @@ export function hasElementChanged($old: VirtualElement, $new: VirtualElement) { } export function mountComponent(componentInstance: ComponentInstance) { + componentInstance.id = ++lastComponentId; renderComponent(componentInstance); componentInstance.mountState = MountState.Mounted; return componentInstance.$element; @@ -900,6 +910,42 @@ export function useRef(initial?: T | null) { return byCursor[cursor]; } +export function createContext(defaultValue?: T): Context { + 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(context: Context) { + const [getDefaultValue] = useSignal(context.defaultValue); + + return renderingInstance.context?.[context.contextId] || getDefaultValue; +} + +export function useSignal(initial?: T) { + const signalRef = useRef>>(); + signalRef.current ??= createSignal(initial); + return signalRef.current; +} + export function memo(Component: T, debugKey?: string) { function TeactMemoWrapper(props: Props) { return useMemo( @@ -929,6 +975,10 @@ export function DEBUG_resolveComponentName(Component: FC_withDebug) { return `memo>${DEBUG_contentComponentName}`; } + if (name === 'TeactContextProvider') { + return `context>id${DEBUG_contentComponentName}`; + } + return name + (DEBUG_contentComponentName ? `>${DEBUG_contentComponentName}` : ''); }