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 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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<HTMLDivElement>, isComposerVisible: boolean) {
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 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[];

View File

@ -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';

View File

@ -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';

View File

@ -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';

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';
type Selector<T extends unknown> = (global: GlobalState) => T;

View File

@ -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:

View File

@ -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';

View File

@ -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<Range | undefined>();

View File

@ -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,

View File

@ -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;

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 { Signal } from '../../util/signals';
import type {
VirtualElement,
VirtualElementChildren,
@ -34,6 +35,8 @@ interface SelectionState {
isCaretAtEnd: boolean;
}
type CurrentContext = Record<string, Signal<unknown>>;
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<T extends VirtualElement | undefined>(
$current: VirtualElement | undefined,
$new: T,
$parent: VirtualElementParent | VirtualDomHead,
currentContext: CurrentContext,
index: number,
options: {
skipComponentUpdate?: boolean;
@ -116,7 +120,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
&& 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<T extends VirtualElement | undefined>(
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<T extends VirtualElement | undefined>(
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<T extends VirtualElement | undefined>(
}
}
} 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<T extends VirtualElement | undefined>(
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<T extends VirtualElement | undefined>(
renderChildren(
$current,
$new as VirtualElementComponent | VirtualElementFragment,
currentContext,
parentEl,
nextSibling,
options.forceMoveToEnd,
@ -216,7 +227,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
}
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;
}

View File

@ -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<string, Signal<unknown>>;
hooks?: {
state?: {
cursor: number;
@ -132,12 +133,20 @@ export type TeactNode =
type Effect = () => (NoneToVoidFunction | void);
type EffectCleanup = NoneToVoidFunction;
export type Context<T> = {
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<T>(initial?: T | null) {
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) {
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}` : '');
}