diff --git a/src/components/App.tsx b/src/components/App.tsx index 8effa288b..ff8d7dd16 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -24,7 +24,7 @@ import usePrevious from '../hooks/usePrevious'; import { useSignalEffect } from '../hooks/useSignalEffect'; import { getIsInBackground } from '../hooks/window/useBackgroundMode'; -// import Test from './test/TestSvg'; +// import Test from './test/TestCleanupOrder'; import Auth from './auth/Auth'; import Notifications from './common/Notifications'; import UiLoader from './common/UiLoader'; diff --git a/src/components/test/TestCleanupOrder.tsx b/src/components/test/TestCleanupOrder.tsx index 2eeaede7c..139475e55 100644 --- a/src/components/test/TestCleanupOrder.tsx +++ b/src/components/test/TestCleanupOrder.tsx @@ -1,53 +1,84 @@ -import { useEffect, useLayoutEffect, useState } from '../../lib/teact/teact'; - -const TestCleanupOrder = () => { - const [, setRand] = useState(Math.random()); +/* eslint-disable no-console */ +import { type TeactNode, useEffect, useLayoutEffect, useState } from '../../lib/teact/teact'; +function Component1({ children }: { children?: TeactNode }) { useEffect(() => { - // eslint-disable-next-line no-console - console.log('effect 1'); - - setTimeout(() => { - setRand(Math.random()); - }, 3000); - - return () => { - // eslint-disable-next-line no-console - console.log('cleanup 1'); - }; + console.log('Effect 1'); + return () => console.log('Cleanup 1'); }); useEffect(() => { - // eslint-disable-next-line no-console - console.log('effect 2'); - - return () => { - // eslint-disable-next-line no-console - console.log('cleanup 2'); - }; + console.log('Effect 1.2'); + return () => console.log('Cleanup 1.2'); }); useLayoutEffect(() => { - // eslint-disable-next-line no-console - console.log('layout effect 1'); - - return () => { - // eslint-disable-next-line no-console - console.log('layout cleanup 1'); - }; + console.log('Layout 1'); + return () => console.log('Layout Cleanup 1'); }); useLayoutEffect(() => { - // eslint-disable-next-line no-console - console.log('layout effect 2'); + console.log('Layout 1.2'); + return () => console.log('Layout Cleanup 1.2'); + }); + return children; +} - return () => { - // eslint-disable-next-line no-console - console.log('layout cleanup 2'); - }; +function Component1B({ children }: { children?: TeactNode }) { + useEffect(() => { + console.log('Effect 1b'); + return () => console.log('Cleanup 1b'); }); - return
Test
; -}; + useLayoutEffect(() => { + console.log('Layout 1b'); + return () => console.log('Layout Cleanup 1b'); + }); + return 'B'; +} + +function Component2({ children }: { children?: TeactNode }) { + useEffect(() => { + console.log('Effect 2'); + return () => console.log('Cleanup 2'); + }); + + useLayoutEffect(() => { + console.log('Layout 2'); + return () => console.log('Layout Cleanup 2'); + }); + return children; +} + +function Component3() { + useEffect(() => { + console.log('Effect 3'); + return () => console.log('Cleanup 3'); + }); + + useLayoutEffect(() => { + console.log('Layout 3'); + return () => console.log('Layout Cleanup 3'); + }); + return
Leaf
; +} + +function TestCleanupOrder() { + const [isMounted, setIsMounted] = useState(true); + return ( +
setIsMounted((p) => !p)}> + {isMounted && ( + <> + + + + + + + + )} +
+ ); +} export default TestCleanupOrder; diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 8d1f9a663..0a0146366 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -155,6 +155,7 @@ const Fragment = Symbol('Fragment') as unknown as FC<{ children: TeactNode }>; const DEBUG_RENDER_THRESHOLD = 7; const DEBUG_EFFECT_THRESHOLD = 7; const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']); +const MAX_EFFECT_CURSORS_PER_INSTANCE = 10000; let contextCounter = 0; @@ -315,12 +316,37 @@ if (DEBUG) { let instancesPendingUpdate = new Set(); let idsToExcludeFromUpdate = new Set(); -let pendingEffects = new Map(); -let pendingCleanups = new Map(); -let pendingLayoutEffects = new Map(); -let pendingLayoutCleanups = new Map(); +let pendingEffects = new Map(); +let pendingCleanups = new Map(); +let pendingLayoutEffects = new Map(); +let pendingLayoutCleanups = new Map(); let areImmediateEffectsCaptured = false; +/* + Effect call order: + - Regular call order inside component + - Child to parent + - Parent to child on unmount + - Sibling behavior is not defined +*/ +function runEffectsMap(map: Map) { + Array.from(map.entries()) + .sort(([aKey], [bKey]) => { + const idA = Math.floor(aKey / MAX_EFFECT_CURSORS_PER_INSTANCE); + const idB = Math.floor(bKey / MAX_EFFECT_CURSORS_PER_INSTANCE); + + const idDiff = idB - idA; + if (idDiff !== 0) { + return idDiff; + } + + const cursorA = aKey % MAX_EFFECT_CURSORS_PER_INSTANCE; + const cursorB = bKey % MAX_EFFECT_CURSORS_PER_INSTANCE; + + return cursorA - cursorB; + }).forEach(([_, cb]) => void cb()); +} + /* Order: - component effect cleanups @@ -350,11 +376,11 @@ const runUpdatePassOnRaf = throttleWith(requestMeasure, () => { const currentCleanups = pendingCleanups; pendingCleanups = new Map(); - currentCleanups.forEach((cb) => cb()); + runEffectsMap(currentCleanups); const currentEffects = pendingEffects; pendingEffects = new Map(); - currentEffects.forEach((cb) => cb()); + runEffectsMap(currentEffects); requestMutation(() => { instancesToUpdate.forEach(prepareComponentForFrame); @@ -382,11 +408,11 @@ export function captureImmediateEffects() { function runCapturedImmediateEffects() { const currentLayoutCleanups = pendingLayoutCleanups; pendingLayoutCleanups = new Map(); - currentLayoutCleanups.forEach((cb) => cb()); + runEffectsMap(currentLayoutCleanups); const currentLayoutEffects = pendingLayoutEffects; pendingLayoutEffects = new Map(); - currentLayoutEffects.forEach((cb) => cb()); + runEffectsMap(currentLayoutEffects); areImmediateEffectsCaptured = false; } @@ -738,6 +764,11 @@ function useEffectBase( } renderingInstance.hooks.effects.cursor++; + + if (DEBUG && cursor >= MAX_EFFECT_CURSORS_PER_INSTANCE) { + // eslint-disable-next-line no-console + console.warn(`[Teact] Effect cursor #${cursor} in ${componentInstance.name} is out of bounds. How?`); + } } function scheduleEffect( @@ -750,7 +781,7 @@ function scheduleEffect( const cleanup = byCursor[cursor]?.cleanup; const cleanupsContainer = isLayout ? pendingLayoutCleanups : pendingCleanups; const effectsContainer = isLayout ? pendingLayoutEffects : pendingEffects; - const effectId = `${componentInstance.id}_${cursor}`; + const effectId = componentInstance.id * MAX_EFFECT_CURSORS_PER_INSTANCE + cursor; if (cleanup) { const runEffectCleanup = () => safeExec(() => {