elementdrawing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEffect Hook Implementation
|
|
3
|
+
* ElementDrawing Framework - Effect hooks with dependency tracking,
|
|
4
|
+
* cleanup, scheduling, batching, passive/layout/insertion effects,
|
|
5
|
+
* strict mode support, effect grouping, and cancellation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ─── Effect Types ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const EFFECT_TYPES = {
|
|
13
|
+
PASSIVE: 'passive', // useEffect - runs after paint
|
|
14
|
+
LAYOUT: 'layout', // useLayoutEffect - runs before paint
|
|
15
|
+
INSERTION: 'insertion', // useInsertionEffect - runs before DOM mutations
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ─── Internal State ───────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
let currentlyRenderingComponent = null;
|
|
21
|
+
let hookIndex = 0;
|
|
22
|
+
const effectQueue = [];
|
|
23
|
+
const layoutEffectQueue = [];
|
|
24
|
+
const insertionEffectQueue = [];
|
|
25
|
+
let isFlushingEffects = false;
|
|
26
|
+
let isFlushingLayoutEffects = false;
|
|
27
|
+
let isFlushingInsertionEffects = false;
|
|
28
|
+
|
|
29
|
+
// ─── Strict Mode ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
let isStrictMode = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enable or disable strict mode. In strict mode, effects are double-invoked
|
|
35
|
+
* on mount to help find side-effect cleanup issues.
|
|
36
|
+
* @param {boolean} enabled
|
|
37
|
+
*/
|
|
38
|
+
function setStrictMode(enabled) {
|
|
39
|
+
isStrictMode = enabled;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if strict mode is enabled.
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isInStrictMode() {
|
|
47
|
+
return isStrictMode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Dependency Comparison ────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compare two dependency arrays for equality.
|
|
54
|
+
* @param {Array|null} prevDeps - Previous dependencies
|
|
55
|
+
* @param {Array|null} nextDeps - Next dependencies
|
|
56
|
+
* @returns {boolean} True if dependencies are the same
|
|
57
|
+
*/
|
|
58
|
+
function areDepsEqual(prevDeps, nextDeps) {
|
|
59
|
+
if (prevDeps === null || nextDeps === null) return false;
|
|
60
|
+
if (prevDeps.length !== nextDeps.length) return false;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < prevDeps.length; i++) {
|
|
63
|
+
if (Object.is(prevDeps[i], nextDeps[i])) continue;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Deep dependency comparison for complex dependency values.
|
|
71
|
+
* @param {Array|null} prevDeps
|
|
72
|
+
* @param {Array|null} nextDeps
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
function areDepsDeepEqual(prevDeps, nextDeps) {
|
|
76
|
+
if (prevDeps === null || nextDeps === null) return false;
|
|
77
|
+
if (prevDeps.length !== nextDeps.length) return false;
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < prevDeps.length; i++) {
|
|
80
|
+
if (deepEqualValue(prevDeps[i], nextDeps[i])) continue;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Deep equal comparison for individual values.
|
|
88
|
+
*/
|
|
89
|
+
function deepEqualValue(a, b) {
|
|
90
|
+
if (Object.is(a, b)) return true;
|
|
91
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const keysA = Object.keys(a);
|
|
95
|
+
const keysB = Object.keys(b);
|
|
96
|
+
if (keysA.length !== keysB.length) return false;
|
|
97
|
+
for (const key of keysA) {
|
|
98
|
+
if (!deepEqualValue(a[key], b[key])) return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate that dependencies is an array or undefined.
|
|
105
|
+
* @param {*} deps - Dependencies to validate
|
|
106
|
+
*/
|
|
107
|
+
function validateDeps(deps) {
|
|
108
|
+
if (deps !== undefined && !Array.isArray(deps)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
'[useEffect] Dependencies must be an array or undefined. ' +
|
|
111
|
+
`Received: ${typeof deps}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Effect Execution ─────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute an effect, capturing its cleanup function.
|
|
120
|
+
* @param {Object} effect - Effect object with create function
|
|
121
|
+
* @returns {Function|null} Cleanup function
|
|
122
|
+
*/
|
|
123
|
+
function executeEffect(effect) {
|
|
124
|
+
try {
|
|
125
|
+
const cleanup = effect.create();
|
|
126
|
+
if (cleanup !== undefined && typeof cleanup !== 'function') {
|
|
127
|
+
console.warn(
|
|
128
|
+
'[useEffect] Effect cleanup must be a function or undefined. ' +
|
|
129
|
+
`Received: ${typeof cleanup}`
|
|
130
|
+
);
|
|
131
|
+
effect.cleanup = null;
|
|
132
|
+
} else {
|
|
133
|
+
effect.cleanup = cleanup;
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('[useEffect] Effect execution error:', error);
|
|
137
|
+
effect.cleanup = null;
|
|
138
|
+
|
|
139
|
+
// Call error boundary if available
|
|
140
|
+
if (effect.component && effect.component._handleError) {
|
|
141
|
+
effect.component._handleError(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return effect.cleanup;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Run cleanup for an effect.
|
|
149
|
+
* @param {Object} effect - Effect object with cleanup function
|
|
150
|
+
*/
|
|
151
|
+
function runCleanup(effect) {
|
|
152
|
+
if (typeof effect.cleanup === 'function') {
|
|
153
|
+
try {
|
|
154
|
+
effect.cleanup();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('[useEffect] Cleanup error:', error);
|
|
157
|
+
if (effect.component && effect.component._handleError) {
|
|
158
|
+
effect.component._handleError(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
effect.cleanup = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Execute an effect in strict mode (double-invoke on mount).
|
|
167
|
+
* @param {Object} effect - Effect to execute
|
|
168
|
+
*/
|
|
169
|
+
function executeEffectStrictMode(effect) {
|
|
170
|
+
// First invocation
|
|
171
|
+
executeEffect(effect);
|
|
172
|
+
|
|
173
|
+
// Immediately cleanup and re-invoke in strict mode
|
|
174
|
+
runCleanup(effect);
|
|
175
|
+
executeEffect(effect);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Effect Scheduling ────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Schedule passive effects to run after the browser paint.
|
|
182
|
+
*/
|
|
183
|
+
function schedulePassiveEffects() {
|
|
184
|
+
if (effectQueue.length === 0) return;
|
|
185
|
+
|
|
186
|
+
// Use requestAnimationFrame then setTimeout to ensure effects run after paint
|
|
187
|
+
requestAnimationFrame(() => {
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
flushPassiveEffects();
|
|
190
|
+
}, 0);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Flush all queued passive effects.
|
|
196
|
+
*/
|
|
197
|
+
function flushPassiveEffects() {
|
|
198
|
+
if (isFlushingEffects) return;
|
|
199
|
+
isFlushingEffects = true;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Process all queued passive effects
|
|
203
|
+
while (effectQueue.length > 0) {
|
|
204
|
+
const effect = effectQueue.shift();
|
|
205
|
+
|
|
206
|
+
// Run cleanup from previous render first
|
|
207
|
+
runCleanup(effect);
|
|
208
|
+
|
|
209
|
+
// Execute the new effect
|
|
210
|
+
if (isStrictMode && !effect._hasRunBefore) {
|
|
211
|
+
executeEffectStrictMode(effect);
|
|
212
|
+
effect._hasRunBefore = true;
|
|
213
|
+
} else {
|
|
214
|
+
executeEffect(effect);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
isFlushingEffects = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Flush all queued layout effects (synchronous, before paint).
|
|
224
|
+
*/
|
|
225
|
+
function flushLayoutEffects() {
|
|
226
|
+
if (isFlushingLayoutEffects) return;
|
|
227
|
+
isFlushingLayoutEffects = true;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
while (layoutEffectQueue.length > 0) {
|
|
231
|
+
const effect = layoutEffectQueue.shift();
|
|
232
|
+
runCleanup(effect);
|
|
233
|
+
|
|
234
|
+
if (isStrictMode && !effect._hasRunBefore) {
|
|
235
|
+
executeEffectStrictMode(effect);
|
|
236
|
+
effect._hasRunBefore = true;
|
|
237
|
+
} else {
|
|
238
|
+
executeEffect(effect);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
isFlushingLayoutEffects = false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Flush all queued insertion effects (synchronous, before DOM mutations).
|
|
248
|
+
*/
|
|
249
|
+
function flushInsertionEffects() {
|
|
250
|
+
if (isFlushingInsertionEffects) return;
|
|
251
|
+
isFlushingInsertionEffects = true;
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
while (insertionEffectQueue.length > 0) {
|
|
255
|
+
const effect = insertionEffectQueue.shift();
|
|
256
|
+
runCleanup(effect);
|
|
257
|
+
executeEffect(effect);
|
|
258
|
+
}
|
|
259
|
+
} finally {
|
|
260
|
+
isFlushingInsertionEffects = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Flush effects in the correct order: insertion -> layout -> passive.
|
|
266
|
+
*/
|
|
267
|
+
function flushAllEffects() {
|
|
268
|
+
flushInsertionEffects();
|
|
269
|
+
flushLayoutEffects();
|
|
270
|
+
schedulePassiveEffects();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Effect Cancellation ──────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Cancel a pending effect by reference.
|
|
277
|
+
* @param {Object} effect - The effect to cancel
|
|
278
|
+
* @returns {boolean} True if successfully cancelled
|
|
279
|
+
*/
|
|
280
|
+
function cancelEffect(effect) {
|
|
281
|
+
if (!effect) return false;
|
|
282
|
+
|
|
283
|
+
let removed = false;
|
|
284
|
+
|
|
285
|
+
// Try to remove from passive queue
|
|
286
|
+
const passiveIdx = effectQueue.indexOf(effect);
|
|
287
|
+
if (passiveIdx !== -1) {
|
|
288
|
+
effectQueue.splice(passiveIdx, 1);
|
|
289
|
+
removed = true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Try to remove from layout queue
|
|
293
|
+
const layoutIdx = layoutEffectQueue.indexOf(effect);
|
|
294
|
+
if (layoutIdx !== -1) {
|
|
295
|
+
layoutEffectQueue.splice(layoutIdx, 1);
|
|
296
|
+
removed = true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Try to remove from insertion queue
|
|
300
|
+
const insertionIdx = insertionEffectQueue.indexOf(effect);
|
|
301
|
+
if (insertionIdx !== -1) {
|
|
302
|
+
insertionEffectQueue.splice(insertionIdx, 1);
|
|
303
|
+
removed = true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Run cleanup if the effect had been previously executed
|
|
307
|
+
if (removed) {
|
|
308
|
+
runCleanup(effect);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return removed;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Effect Grouping ──────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Group effects together so they execute as a batch.
|
|
318
|
+
* @param {Function} callback - Function containing effectful operations
|
|
319
|
+
* @returns {Array} Array of effect objects created in the group
|
|
320
|
+
*/
|
|
321
|
+
const activeEffectGroup = { current: null };
|
|
322
|
+
|
|
323
|
+
function beginEffectGroup() {
|
|
324
|
+
activeEffectGroup.current = [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function endEffectGroup() {
|
|
328
|
+
const group = activeEffectGroup.current;
|
|
329
|
+
activeEffectGroup.current = null;
|
|
330
|
+
return group;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ─── Hook Context Management ──────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
function setCurrentComponent(component) {
|
|
336
|
+
currentlyRenderingComponent = component;
|
|
337
|
+
hookIndex = 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function resetHookIndex() {
|
|
341
|
+
hookIndex = 0;
|
|
342
|
+
currentlyRenderingComponent = null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─── Core Effect Hook ─────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Internal implementation shared by all effect hooks.
|
|
349
|
+
*
|
|
350
|
+
* @param {Function} create - Effect callback (returns cleanup or undefined)
|
|
351
|
+
* @param {Array|undefined} deps - Dependency array
|
|
352
|
+
* @param {string} effectType - Type of effect (passive, layout, insertion)
|
|
353
|
+
* @returns {void}
|
|
354
|
+
*/
|
|
355
|
+
function useEffectImpl(create, deps, effectType) {
|
|
356
|
+
const component = currentlyRenderingComponent;
|
|
357
|
+
if (!component) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`[use${effectType}Effect] Must be called within a component render phase`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (typeof create !== 'function') {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`[use${effectType}Effect] First argument must be a function. ` +
|
|
366
|
+
`Received: ${typeof create}`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
validateDeps(deps);
|
|
371
|
+
|
|
372
|
+
// Initialize hooks array
|
|
373
|
+
if (!component._hooks) {
|
|
374
|
+
component._hooks = [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const currentHookIndex = hookIndex;
|
|
378
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
379
|
+
let hook;
|
|
380
|
+
|
|
381
|
+
if (component._hooks[currentHookIndex]) {
|
|
382
|
+
// Re-render: check if dependencies changed
|
|
383
|
+
hook = component._hooks[currentHookIndex];
|
|
384
|
+
|
|
385
|
+
const prevDeps = hook.deps;
|
|
386
|
+
const depsChanged = !areDepsEqual(prevDeps, resolvedDeps);
|
|
387
|
+
|
|
388
|
+
if (depsChanged) {
|
|
389
|
+
// Dependencies changed - queue the effect
|
|
390
|
+
hook.deps = resolvedDeps;
|
|
391
|
+
hook.create = create;
|
|
392
|
+
hook.type = effectType;
|
|
393
|
+
hook.component = component;
|
|
394
|
+
hook._shouldRun = true;
|
|
395
|
+
} else {
|
|
396
|
+
// Dependencies unchanged - skip the effect
|
|
397
|
+
hook._shouldRun = false;
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
// First render - always run the effect
|
|
401
|
+
hook = {
|
|
402
|
+
type: effectType,
|
|
403
|
+
create,
|
|
404
|
+
cleanup: null,
|
|
405
|
+
deps: resolvedDeps,
|
|
406
|
+
component,
|
|
407
|
+
_shouldRun: true,
|
|
408
|
+
_isMounted: false,
|
|
409
|
+
_hasRunBefore: false,
|
|
410
|
+
_createdAt: Date.now(),
|
|
411
|
+
_effectId: ++effectIdCounter,
|
|
412
|
+
};
|
|
413
|
+
component._hooks[currentHookIndex] = hook;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Queue the effect if it should run
|
|
417
|
+
if (hook._shouldRun) {
|
|
418
|
+
switch (effectType) {
|
|
419
|
+
case EFFECT_TYPES.PASSIVE:
|
|
420
|
+
effectQueue.push(hook);
|
|
421
|
+
break;
|
|
422
|
+
case EFFECT_TYPES.LAYOUT:
|
|
423
|
+
layoutEffectQueue.push(hook);
|
|
424
|
+
break;
|
|
425
|
+
case EFFECT_TYPES.INSERTION:
|
|
426
|
+
insertionEffectQueue.push(hook);
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Track in active effect group if one exists
|
|
431
|
+
if (activeEffectGroup.current) {
|
|
432
|
+
activeEffectGroup.current.push(hook);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
hookIndex++;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let effectIdCounter = 0;
|
|
440
|
+
|
|
441
|
+
// ─── Public Hooks ─────────────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* useEffect - Schedule a passive effect that runs after the browser paint.
|
|
445
|
+
* Ideal for side effects that don't need to block visual updates.
|
|
446
|
+
*
|
|
447
|
+
* @param {Function} create - Effect callback, may return a cleanup function
|
|
448
|
+
* @param {Array} [deps] - Dependency array; effect re-runs when deps change
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* useEffect(() => {
|
|
452
|
+
* const subscription = api.subscribe(data => setData(data));
|
|
453
|
+
* return () => subscription.unsubscribe();
|
|
454
|
+
* }, [api]);
|
|
455
|
+
*/
|
|
456
|
+
function useEffect(create, deps) {
|
|
457
|
+
useEffectImpl(create, deps, EFFECT_TYPES.PASSIVE);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* useLayoutEffect - Schedule a layout effect that runs synchronously after
|
|
462
|
+
* all DOM mutations but before the browser paint.
|
|
463
|
+
*
|
|
464
|
+
* @param {Function} create - Effect callback, may return a cleanup function
|
|
465
|
+
* @param {Array} [deps] - Dependency array
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* useLayoutEffect(() => {
|
|
469
|
+
* const height = ref.current.clientHeight;
|
|
470
|
+
* setTooltipPosition(calculatePosition(height));
|
|
471
|
+
* }, [ref.current]);
|
|
472
|
+
*/
|
|
473
|
+
function useLayoutEffect(create, deps) {
|
|
474
|
+
useEffectImpl(create, deps, EFFECT_TYPES.LAYOUT);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* useInsertionEffect - Schedule an insertion effect that runs before DOM
|
|
479
|
+
* mutations. Primarily for CSS-in-JS libraries.
|
|
480
|
+
*
|
|
481
|
+
* @param {Function} create - Effect callback, may return a cleanup function
|
|
482
|
+
* @param {Array} [deps] - Dependency array
|
|
483
|
+
*/
|
|
484
|
+
function useInsertionEffect(create, deps) {
|
|
485
|
+
useEffectImpl(create, deps, EFFECT_TYPES.INSERTION);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ─── Custom Effect Hooks ──────────────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* useDeepCompareEffect - Like useEffect but uses deep comparison for deps.
|
|
492
|
+
* Useful when dependencies are objects or arrays that may be recreated.
|
|
493
|
+
*
|
|
494
|
+
* @param {Function} create - Effect callback
|
|
495
|
+
* @param {Array} deps - Dependency array (compared deeply)
|
|
496
|
+
*/
|
|
497
|
+
function useDeepCompareEffect(create, deps) {
|
|
498
|
+
const component = currentlyRenderingComponent;
|
|
499
|
+
if (!component) {
|
|
500
|
+
throw new Error('[useDeepCompareEffect] Must be called within a component render phase');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!component._hooks) {
|
|
504
|
+
component._hooks = [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const currentHookIndex = hookIndex;
|
|
508
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
509
|
+
let hook;
|
|
510
|
+
|
|
511
|
+
if (component._hooks[currentHookIndex]) {
|
|
512
|
+
hook = component._hooks[currentHookIndex];
|
|
513
|
+
|
|
514
|
+
const prevDeps = hook.deps;
|
|
515
|
+
const depsChanged = !areDepsDeepEqual(prevDeps, resolvedDeps);
|
|
516
|
+
|
|
517
|
+
if (depsChanged) {
|
|
518
|
+
hook.deps = resolvedDeps;
|
|
519
|
+
hook.create = create;
|
|
520
|
+
hook._shouldRun = true;
|
|
521
|
+
} else {
|
|
522
|
+
hook._shouldRun = false;
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
hook = {
|
|
526
|
+
type: EFFECT_TYPES.PASSIVE,
|
|
527
|
+
create,
|
|
528
|
+
cleanup: null,
|
|
529
|
+
deps: resolvedDeps,
|
|
530
|
+
component,
|
|
531
|
+
_shouldRun: true,
|
|
532
|
+
_isMounted: false,
|
|
533
|
+
_hasRunBefore: false,
|
|
534
|
+
_createdAt: Date.now(),
|
|
535
|
+
_effectId: ++effectIdCounter,
|
|
536
|
+
};
|
|
537
|
+
component._hooks[currentHookIndex] = hook;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (hook._shouldRun) {
|
|
541
|
+
effectQueue.push(hook);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
hookIndex++;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* useEffectOnce - Effect that only runs once on mount, like componentDidMount.
|
|
549
|
+
* @param {Function} create - Effect callback
|
|
550
|
+
*/
|
|
551
|
+
function useEffectOnce(create) {
|
|
552
|
+
useEffect(create, []);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* useUpdateEffect - Effect that skips the first render (only runs on updates).
|
|
557
|
+
* @param {Function} create - Effect callback
|
|
558
|
+
* @param {Array} deps - Dependency array
|
|
559
|
+
*/
|
|
560
|
+
function useUpdateEffect(create, deps) {
|
|
561
|
+
const component = currentlyRenderingComponent;
|
|
562
|
+
if (!component) {
|
|
563
|
+
throw new Error('[useUpdateEffect] Must be called within a component render phase');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!component._hooks) {
|
|
567
|
+
component._hooks = [];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const currentHookIndex = hookIndex;
|
|
571
|
+
let hook;
|
|
572
|
+
|
|
573
|
+
if (component._hooks[currentHookIndex]) {
|
|
574
|
+
hook = component._hooks[currentHookIndex];
|
|
575
|
+
} else {
|
|
576
|
+
hook = {
|
|
577
|
+
_isFirstRender: true,
|
|
578
|
+
};
|
|
579
|
+
component._hooks[currentHookIndex] = hook;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const isFirst = hook._isFirstRender;
|
|
583
|
+
hook._isFirstRender = false;
|
|
584
|
+
|
|
585
|
+
if (isFirst) {
|
|
586
|
+
// Skip the first render, but still track deps
|
|
587
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
588
|
+
hook.deps = resolvedDeps;
|
|
589
|
+
hookIndex++;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
useEffectImpl(create, deps, EFFECT_TYPES.PASSIVE);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ─── Effect Batching ──────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Batch multiple effectful operations. All effects created within the
|
|
600
|
+
* callback will be collected and flushed together.
|
|
601
|
+
*
|
|
602
|
+
* @param {Function} callback - Function containing effectful operations
|
|
603
|
+
* @returns {*} Return value of the callback
|
|
604
|
+
*/
|
|
605
|
+
function batchEffects(callback) {
|
|
606
|
+
const prevFlushingState = isFlushingEffects;
|
|
607
|
+
isFlushingEffects = true;
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
return callback();
|
|
611
|
+
} finally {
|
|
612
|
+
isFlushingEffects = prevFlushingState;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ─── Effect Debug & Inspection ────────────────────────────────────────────────
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Get the count of pending effects across all queues.
|
|
620
|
+
* @returns {Object} Counts for each effect type
|
|
621
|
+
*/
|
|
622
|
+
function getPendingEffectCounts() {
|
|
623
|
+
return {
|
|
624
|
+
passive: effectQueue.length,
|
|
625
|
+
layout: layoutEffectQueue.length,
|
|
626
|
+
insertion: insertionEffectQueue.length,
|
|
627
|
+
total: effectQueue.length + layoutEffectQueue.length + insertionEffectQueue.length,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get all effects for a specific component.
|
|
633
|
+
* @param {Object} component
|
|
634
|
+
* @returns {Array} Array of effect objects
|
|
635
|
+
*/
|
|
636
|
+
function getComponentEffects(component) {
|
|
637
|
+
if (!component._hooks) return [];
|
|
638
|
+
|
|
639
|
+
return component._hooks.filter((hook) =>
|
|
640
|
+
hook && (hook.type === EFFECT_TYPES.PASSIVE ||
|
|
641
|
+
hook.type === EFFECT_TYPES.LAYOUT ||
|
|
642
|
+
hook.type === EFFECT_TYPES.INSERTION)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ─── Cleanup on Unmount ──────────────────────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Clean up all effects for a component when it unmounts.
|
|
650
|
+
* @param {Object} component - The unmounting component
|
|
651
|
+
*/
|
|
652
|
+
function cleanupComponentEffects(component) {
|
|
653
|
+
if (!component._hooks) return;
|
|
654
|
+
|
|
655
|
+
component._hooks.forEach((hook) => {
|
|
656
|
+
if (hook && typeof hook.cleanup === 'function') {
|
|
657
|
+
runCleanup(hook);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Remove effects from queues
|
|
662
|
+
for (let i = effectQueue.length - 1; i >= 0; i--) {
|
|
663
|
+
if (effectQueue[i].component === component) {
|
|
664
|
+
effectQueue.splice(i, 1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (let i = layoutEffectQueue.length - 1; i >= 0; i--) {
|
|
668
|
+
if (layoutEffectQueue[i].component === component) {
|
|
669
|
+
layoutEffectQueue.splice(i, 1);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
for (let i = insertionEffectQueue.length - 1; i >= 0; i--) {
|
|
673
|
+
if (insertionEffectQueue[i].component === component) {
|
|
674
|
+
insertionEffectQueue.splice(i, 1);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
680
|
+
|
|
681
|
+
module.exports = {
|
|
682
|
+
useEffect,
|
|
683
|
+
useLayoutEffect,
|
|
684
|
+
useInsertionEffect,
|
|
685
|
+
useDeepCompareEffect,
|
|
686
|
+
useEffectOnce,
|
|
687
|
+
useUpdateEffect,
|
|
688
|
+
setCurrentComponent,
|
|
689
|
+
resetHookIndex,
|
|
690
|
+
flushPassiveEffects,
|
|
691
|
+
flushLayoutEffects,
|
|
692
|
+
flushInsertionEffects,
|
|
693
|
+
flushAllEffects,
|
|
694
|
+
batchEffects,
|
|
695
|
+
areDepsEqual,
|
|
696
|
+
areDepsDeepEqual,
|
|
697
|
+
cancelEffect,
|
|
698
|
+
setStrictMode,
|
|
699
|
+
isInStrictMode,
|
|
700
|
+
getPendingEffectCounts,
|
|
701
|
+
getComponentEffects,
|
|
702
|
+
cleanupComponentEffects,
|
|
703
|
+
EFFECT_TYPES,
|
|
704
|
+
};
|