Skip to content

Commit c3b6075

Browse files
committed
Support signals without importing preact-hooks
1 parent eae850a commit c3b6075

File tree

2 files changed

+115
-73
lines changed

2 files changed

+115
-73
lines changed

packages/preact/src/index.ts

Lines changed: 102 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { options, Component, isValidElement, Fragment } from "preact";
2-
import { useRef, useMemo, useEffect } from "preact/hooks";
32
import {
43
signal,
54
computed,
@@ -18,6 +17,7 @@ import {
1817
PropertyUpdater,
1918
AugmentedComponent,
2019
AugmentedElement as Element,
20+
HookState,
2121
} from "./internal";
2222

2323
export {
@@ -53,6 +53,7 @@ function hook<T extends OptionsTypes>(hookName: T, hookFn: HookFn<T>) {
5353

5454
let currentComponent: AugmentedComponent | undefined;
5555
let finishUpdate: (() => void) | undefined;
56+
let setupTasks: AugmentedComponent[] = [];
5657

5758
function setCurrentUpdater(updater?: Effect) {
5859
// end tracking for the current update:
@@ -91,7 +92,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) {
9192
const currentSignal = useSignal(data);
9293
currentSignal.value = data;
9394

94-
const [isText, s] = useMemo(() => {
95+
const [isText, s] = useStoreOnce(() => {
9596
let self = this;
9697
// mark the parent component as having computeds so it gets optimized
9798
let v = this.__v;
@@ -138,7 +139,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) {
138139
};
139140

140141
return [isText, wrappedSignal];
141-
}, []);
142+
});
142143

143144
// Rerender the component whenever `data.value` changes from a VNode
144145
// to another VNode, from text to a VNode, or from a VNode to text.
@@ -210,6 +211,7 @@ hook(OptionsTypes.RENDER, (old, vnode) => {
210211
}
211212
}
212213

214+
currentHookIndex = 0;
213215
currentComponent = component;
214216
setCurrentUpdater(updater);
215217
}
@@ -262,7 +264,16 @@ hook(OptionsTypes.DIFFED, (old, vnode) => {
262264
}
263265
}
264266
}
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+
}
265275
}
276+
266277
old(vnode);
267278
});
268279

@@ -326,8 +337,15 @@ hook(OptionsTypes.UNMOUNT, (old, vnode: VNode) => {
326337
component._updater = undefined;
327338
updater._dispose();
328339
}
340+
341+
const persistentState = component.__persistentState;
342+
if (persistentState) {
343+
// Cleanup all the stored effects
344+
persistentState._list.forEach(invokeCleanup);
345+
}
329346
}
330347
}
348+
331349
old(vnode);
332350
});
333351

@@ -386,17 +404,16 @@ Component.prototype.shouldComponentUpdate = function (
386404
export function useSignal<T>(value: T, options?: SignalOptions<T>): Signal<T>;
387405
export function useSignal<T = undefined>(): Signal<T | undefined>;
388406
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)
392409
);
393410
}
394411

395412
export function useComputed<T>(compute: () => T, options?: SignalOptions<T>) {
396413
const $compute = useRef(compute);
397414
$compute.current = compute;
398415
(currentComponent as AugmentedComponent)._updateFlags |= HAS_COMPUTEDS;
399-
return useMemo(() => computed<T>(() => $compute.current(), options), []);
416+
return useStoreOnce(() => computed<T>(() => $compute.current(), options));
400417
}
401418

402419
function safeRaf(callback: () => void) {
@@ -434,6 +451,31 @@ function notifyEffects(this: Effect) {
434451
}
435452
}
436453

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+
437479
function flushDomUpdates() {
438480
batch(() => {
439481
let inst: Effect | undefined;
@@ -453,78 +495,65 @@ export function useSignalEffect(cb: () => void | (() => void)) {
453495
const callback = useRef(cb);
454496
callback.current = cb;
455497

456-
useEffect(() => {
498+
useOnce(() => {
457499
return effect(function (this: Effect) {
458500
this._notify = notifyEffects;
459501
return callback.current();
460502
});
461-
}, []);
503+
});
462504
}
463505

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+
}
471511

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;
528558
}
529559
}
530-
*/

packages/preact/src/internal.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ export interface AugmentedComponent extends Component<any, any> {
2727
__v: VNode;
2828
_updater?: Effect;
2929
_updateFlags: number;
30+
__persistentState: SignalState;
31+
}
32+
33+
export interface HookState {
34+
_executed?: boolean;
35+
_stored?: boolean;
36+
_value?: any;
37+
_cleanup?: () => void;
38+
}
39+
40+
export interface SignalState {
41+
_list: HookState[];
42+
_pendingSetup: HookState[];
3043
}
3144

3245
export interface VNode<P = any> extends preact.VNode<P> {

0 commit comments

Comments
 (0)