1
1
import { options , Component , isValidElement , Fragment } from "preact" ;
2
- import { useRef , useMemo , useEffect } from "preact/hooks" ;
3
2
import {
4
3
signal ,
5
4
computed ,
@@ -18,6 +17,7 @@ import {
18
17
PropertyUpdater ,
19
18
AugmentedComponent ,
20
19
AugmentedElement as Element ,
20
+ HookState ,
21
21
} from "./internal" ;
22
22
23
23
export {
@@ -53,6 +53,7 @@ function hook<T extends OptionsTypes>(hookName: T, hookFn: HookFn<T>) {
53
53
54
54
let currentComponent : AugmentedComponent | undefined ;
55
55
let finishUpdate : ( ( ) => void ) | undefined ;
56
+ let setupTasks : AugmentedComponent [ ] = [ ] ;
56
57
57
58
function setCurrentUpdater ( updater ?: Effect ) {
58
59
// end tracking for the current update:
@@ -91,7 +92,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) {
91
92
const currentSignal = useSignal ( data ) ;
92
93
currentSignal . value = data ;
93
94
94
- const [ isText , s ] = useMemo ( ( ) => {
95
+ const [ isText , s ] = useStoreOnce ( ( ) => {
95
96
let self = this ;
96
97
// mark the parent component as having computeds so it gets optimized
97
98
let v = this . __v ;
@@ -138,7 +139,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) {
138
139
} ;
139
140
140
141
return [ isText , wrappedSignal ] ;
141
- } , [ ] ) ;
142
+ } ) ;
142
143
143
144
// Rerender the component whenever `data.value` changes from a VNode
144
145
// to another VNode, from text to a VNode, or from a VNode to text.
@@ -210,6 +211,7 @@ hook(OptionsTypes.RENDER, (old, vnode) => {
210
211
}
211
212
}
212
213
214
+ currentHookIndex = 0 ;
213
215
currentComponent = component ;
214
216
setCurrentUpdater ( updater ) ;
215
217
}
@@ -262,7 +264,16 @@ hook(OptionsTypes.DIFFED, (old, vnode) => {
262
264
}
263
265
}
264
266
}
267
+ } else if ( vnode . __c ) {
268
+ let component = vnode . __c as AugmentedComponent ;
269
+ if (
270
+ component . __persistentState &&
271
+ component . __persistentState . _pendingSetup . length
272
+ ) {
273
+ queueSetupTasks ( setupTasks . push ( component ) ) ;
274
+ }
265
275
}
276
+
266
277
old ( vnode ) ;
267
278
} ) ;
268
279
@@ -326,8 +337,15 @@ hook(OptionsTypes.UNMOUNT, (old, vnode: VNode) => {
326
337
component . _updater = undefined ;
327
338
updater . _dispose ( ) ;
328
339
}
340
+
341
+ const persistentState = component . __persistentState ;
342
+ if ( persistentState ) {
343
+ // Cleanup all the stored effects
344
+ persistentState . _list . forEach ( invokeCleanup ) ;
345
+ }
329
346
}
330
347
}
348
+
331
349
old ( vnode ) ;
332
350
} ) ;
333
351
@@ -386,17 +404,16 @@ Component.prototype.shouldComponentUpdate = function (
386
404
export function useSignal < T > ( value : T , options ?: SignalOptions < T > ) : Signal < T > ;
387
405
export function useSignal < T = undefined > ( ) : Signal < T | undefined > ;
388
406
export function useSignal < T > ( value ?: T , options ?: SignalOptions < T > ) {
389
- return useMemo (
390
- ( ) => signal < T | undefined > ( value , options as SignalOptions ) ,
391
- [ ]
407
+ return useStoreOnce ( ( ) =>
408
+ signal < T | undefined > ( value , options as SignalOptions )
392
409
) ;
393
410
}
394
411
395
412
export function useComputed < T > ( compute : ( ) => T , options ?: SignalOptions < T > ) {
396
413
const $compute = useRef ( compute ) ;
397
414
$compute . current = compute ;
398
415
( currentComponent as AugmentedComponent ) . _updateFlags |= HAS_COMPUTEDS ;
399
- return useMemo ( ( ) => computed < T > ( ( ) => $compute . current ( ) , options ) , [ ] ) ;
416
+ return useStoreOnce ( ( ) => computed < T > ( ( ) => $compute . current ( ) , options ) ) ;
400
417
}
401
418
402
419
function safeRaf ( callback : ( ) => void ) {
@@ -434,6 +451,31 @@ function notifyEffects(this: Effect) {
434
451
}
435
452
}
436
453
454
+ let prevRaf : typeof options . requestAnimationFrame | undefined ;
455
+ function queueSetupTasks ( newQueueLength : number ) {
456
+ if ( newQueueLength === 1 || prevRaf !== options . requestAnimationFrame ) {
457
+ prevRaf = options . requestAnimationFrame ;
458
+ ( prevRaf || deferEffects ) ( flushSetup ) ;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * After paint effects consumer.
464
+ */
465
+ function flushSetup ( ) {
466
+ let component ;
467
+ while ( ( component = setupTasks . shift ( ) ) ) {
468
+ if ( ! component . __persistentState ) continue ;
469
+ try {
470
+ component . __persistentState . _pendingSetup . forEach ( invokeCleanup ) ;
471
+ component . __persistentState . _pendingSetup . forEach ( invokeEffect ) ;
472
+ component . __persistentState . _pendingSetup = [ ] ;
473
+ } catch ( e ) {
474
+ component . __persistentState . _pendingSetup = [ ] ;
475
+ }
476
+ }
477
+ }
478
+
437
479
function flushDomUpdates ( ) {
438
480
batch ( ( ) => {
439
481
let inst : Effect | undefined ;
@@ -453,78 +495,65 @@ export function useSignalEffect(cb: () => void | (() => void)) {
453
495
const callback = useRef ( cb ) ;
454
496
callback . current = cb ;
455
497
456
- useEffect ( ( ) => {
498
+ useOnce ( ( ) => {
457
499
return effect ( function ( this : Effect ) {
458
500
this . _notify = notifyEffects ;
459
501
return callback . current ( ) ;
460
502
} ) ;
461
- } , [ ] ) ;
503
+ } ) ;
462
504
}
463
505
464
- /**
465
- * @todo Determine which Reactive implementation we'll be using.
466
- * @internal
467
- */
468
- // export function useReactive<T extends object>(value: T): Reactive<T> {
469
- // return useMemo(() => reactive<T>(value), []);
470
- // }
506
+ let currentHookIndex = 0 ;
507
+ function getState ( index : number ) : HookState {
508
+ if ( ! currentComponent ) {
509
+ throw new Error ( "Hooks can only be called inside components" ) ;
510
+ }
471
511
472
- /**
473
- * @internal
474
- * Update a Reactive's using the properties of an object or other Reactive.
475
- * Also works for Signals.
476
- * @example
477
- * // Update a Reactive with Object.assign()-like syntax:
478
- * const r = reactive({ name: "Alice" });
479
- * update(r, { name: "Bob" });
480
- * update(r, { age: 42 }); // property 'age' does not exist in type '{ name?: string }'
481
- * update(r, 2); // '2' has no properties in common with '{ name?: string }'
482
- * console.log(r.name.value); // "Bob"
483
- *
484
- * @example
485
- * // Update a Reactive with the properties of another Reactive:
486
- * const A = reactive({ name: "Alice" });
487
- * const B = reactive({ name: "Bob", age: 42 });
488
- * update(A, B);
489
- * console.log(`${A.name} is ${A.age}`); // "Bob is 42"
490
- *
491
- * @example
492
- * // Update a signal with assign()-like syntax:
493
- * const s = signal(42);
494
- * update(s, "hi"); // Argument type 'string' not assignable to type 'number'
495
- * update(s, {}); // Argument type '{}' not assignable to type 'number'
496
- * update(s, 43);
497
- * console.log(s.value); // 43
498
- *
499
- * @param obj The Reactive or Signal to be updated
500
- * @param update The value, Signal, object or Reactive to update `obj` to match
501
- * @param overwrite If `true`, any properties `obj` missing from `update` are set to `undefined`
502
- */
503
- /*
504
- export function update<T extends SignalOrReactive>(
505
- obj: T,
506
- update: Partial<Unwrap<T>>,
507
- overwrite = false
508
- ) {
509
- if (obj instanceof Signal) {
510
- obj.value = peekValue(update);
511
- } else {
512
- for (let i in update) {
513
- if (i in obj) {
514
- obj[i].value = peekValue(update[i]);
515
- } else {
516
- let sig = signal(peekValue(update[i]));
517
- sig[KEY] = i;
518
- obj[i] = sig;
519
- }
520
- }
521
- if (overwrite) {
522
- for (let i in obj) {
523
- if (!(i in update)) {
524
- obj[i].value = undefined;
525
- }
526
- }
527
- }
512
+ const hooks =
513
+ currentComponent . __persistentState ||
514
+ ( currentComponent . __persistentState = {
515
+ _list : [ ] ,
516
+ _pendingSetup : [ ] ,
517
+ } ) ;
518
+
519
+ if ( index >= hooks . _list . length ) {
520
+ hooks . _list . push ( { } ) ;
521
+ }
522
+
523
+ return hooks . _list [ index ] ;
524
+ }
525
+
526
+ function useStoreOnce < T > ( factory : ( ) => T ) : T {
527
+ const state = getState ( currentHookIndex ++ ) ;
528
+ if ( ! state . _stored ) {
529
+ state . _stored = true ;
530
+ state . _value = factory ( ) ;
531
+ }
532
+ return state . _value ;
533
+ }
534
+
535
+ function useRef < T > ( initialValue : T ) : { current : T } {
536
+ return useStoreOnce ( ( ) => ( { current : initialValue } ) ) ;
537
+ }
538
+
539
+ function useOnce ( callback : ( ) => void | ( ( ) => void ) ) : void {
540
+ const state = getState ( currentHookIndex ++ ) ;
541
+ if ( ! state . _executed ) {
542
+ state . _executed = true ;
543
+ state . _value = callback ;
544
+ currentComponent ! . __persistentState . _pendingSetup . push ( state ) ;
545
+ }
546
+ }
547
+
548
+ function invokeEffect ( hook : HookState ) : void {
549
+ if ( hook . _value ) {
550
+ hook . _cleanup = hook . _value ( ) || undefined ;
551
+ }
552
+ }
553
+
554
+ function invokeCleanup ( hook : HookState ) : void {
555
+ if ( hook . _cleanup ) {
556
+ hook . _cleanup ( ) ;
557
+ hook . _cleanup = undefined ;
528
558
}
529
559
}
530
- */
0 commit comments