Teact: Align effect order with other libraries (#6685)

This commit is contained in:
zubiden 2026-02-22 23:43:18 +01:00 committed by Alexander Zinchuk
parent 846ec883d4
commit 1f0ac5b276
3 changed files with 109 additions and 47 deletions

View File

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

View File

@ -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 <div>Test</div>;
};
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 <div>Leaf</div>;
}
function TestCleanupOrder() {
const [isMounted, setIsMounted] = useState(true);
return (
<div style="padding: 1rem" onClick={() => setIsMounted((p) => !p)}>
{isMounted && (
<>
<Component1>
<Component2>
<Component3 />
</Component2>
</Component1>
<Component1B />
</>
)}
</div>
);
}
export default TestCleanupOrder;

View File

@ -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<ComponentInstance>();
let idsToExcludeFromUpdate = new Set<number>();
let pendingEffects = new Map<string, Effect>();
let pendingCleanups = new Map<string, EffectCleanup>();
let pendingLayoutEffects = new Map<string, Effect>();
let pendingLayoutCleanups = new Map<string, EffectCleanup>();
let pendingEffects = new Map<number, Effect>();
let pendingCleanups = new Map<number, EffectCleanup>();
let pendingLayoutEffects = new Map<number, Effect>();
let pendingLayoutCleanups = new Map<number, EffectCleanup>();
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<T extends NoneToVoidFunction>(map: Map<number, T>) {
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(() => {