Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions mangle.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
"props": {
"cname": 6,
"props": {
"$__persistentState": "__$p",
"$_list": "__",
"$_pendingSetup": "__h",
"$_cleanup": "__c",
"$_stateValue": "__",
"$_args": "__H",
"$_stored": "__s",
"$_renderCallbacks": "__h",
"$_skipEffects": "__s",
"core: Node": "",
"$_watched": "W",
"$_unwatched": "Z",
Expand Down
180 changes: 107 additions & 73 deletions packages/preact/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { options, Component, isValidElement, Fragment } from "preact";
import { useRef, useMemo, useEffect } from "preact/hooks";
import {
signal,
computed,
Expand All @@ -18,6 +17,7 @@ import {
PropertyUpdater,
AugmentedComponent,
AugmentedElement as Element,
HookState,
} from "./internal";

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

let currentComponent: AugmentedComponent | undefined;
let finishUpdate: (() => void) | undefined;
let setupTasks: AugmentedComponent[] = [];

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

const [isText, s] = useMemo(() => {
const [isText, s] = useStoreValueOnce(() => {
let self = this;
// mark the parent component as having computeds so it gets optimized
let v = this.__v;
Expand Down Expand Up @@ -138,7 +139,7 @@ function SignalValue(this: AugmentedComponent, { data }: { data: Signal }) {
};

return [isText, wrappedSignal];
}, []);
});

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

currentHookIndex = 0;
currentComponent = component;
setCurrentUpdater(updater);
}
Expand Down Expand Up @@ -262,7 +264,17 @@ hook(OptionsTypes.DIFFED, (old, vnode) => {
}
}
}
} else if (vnode.__c) {
let component = vnode.__c as AugmentedComponent;
if (
component.__persistentState &&
component.__persistentState._pendingSetup.length
) {
console.log("f", component.__persistentState._pendingSetup);
queueSetupTasks(setupTasks.push(component));
}
}

old(vnode);
});

Expand Down Expand Up @@ -326,8 +338,15 @@ hook(OptionsTypes.UNMOUNT, (old, vnode: VNode) => {
component._updater = undefined;
updater._dispose();
}

const persistentState = component.__persistentState;
if (persistentState) {
// Cleanup all the stored effects
persistentState._list.forEach(invokeCleanup);
}
}
}

old(vnode);
});

Expand Down Expand Up @@ -386,17 +405,18 @@ Component.prototype.shouldComponentUpdate = function (
export function useSignal<T>(value: T, options?: SignalOptions<T>): Signal<T>;
export function useSignal<T = undefined>(): Signal<T | undefined>;
export function useSignal<T>(value?: T, options?: SignalOptions<T>) {
return useMemo(
() => signal<T | undefined>(value, options as SignalOptions),
[]
return useStoreValueOnce(() =>
signal<T | undefined>(value, options as SignalOptions)
);
}

export function useComputed<T>(compute: () => T, options?: SignalOptions<T>) {
const $compute = useRef(compute);
$compute.current = compute;
(currentComponent as AugmentedComponent)._updateFlags |= HAS_COMPUTEDS;
return useMemo(() => computed<T>(() => $compute.current(), options), []);
return useStoreValueOnce(() =>
computed<T>(() => $compute.current(), options)
);
}

function safeRaf(callback: () => void) {
Expand Down Expand Up @@ -434,6 +454,32 @@ function notifyEffects(this: Effect) {
}
}

let prevRaf: typeof options.requestAnimationFrame | undefined;
function queueSetupTasks(newQueueLength: number) {
if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
prevRaf = options.requestAnimationFrame;
(prevRaf || deferEffects)(flushSetup);
}
}

/**
* After paint effects consumer.
*/
function flushSetup() {
let component;
while ((component = setupTasks.shift())) {
console.log("flushSetup", component.__P, component.__persistentState);
if (!component.__persistentState || !component.__P) continue;
try {
component.__persistentState._pendingSetup.forEach(invokeCleanup);
component.__persistentState._pendingSetup.forEach(invokeEffect);
component.__persistentState._pendingSetup = [];
} catch (e) {
component.__persistentState._pendingSetup = [];
}
}
}

function flushDomUpdates() {
batch(() => {
let inst: Effect | undefined;
Expand All @@ -453,78 +499,66 @@ export function useSignalEffect(cb: () => void | (() => void)) {
const callback = useRef(cb);
callback.current = cb;

useEffect(() => {
useOnce(() => {
return effect(function (this: Effect) {
this._notify = notifyEffects;
return callback.current();
});
}, []);
});
}

/**
* @todo Determine which Reactive implementation we'll be using.
* @internal
*/
// export function useReactive<T extends object>(value: T): Reactive<T> {
// return useMemo(() => reactive<T>(value), []);
// }
let currentHookIndex = 0;
function getState(index: number): HookState {
if (!currentComponent) {
throw new Error("Hooks can only be called inside components");
}

/**
* @internal
* Update a Reactive's using the properties of an object or other Reactive.
* Also works for Signals.
* @example
* // Update a Reactive with Object.assign()-like syntax:
* const r = reactive({ name: "Alice" });
* update(r, { name: "Bob" });
* update(r, { age: 42 }); // property 'age' does not exist in type '{ name?: string }'
* update(r, 2); // '2' has no properties in common with '{ name?: string }'
* console.log(r.name.value); // "Bob"
*
* @example
* // Update a Reactive with the properties of another Reactive:
* const A = reactive({ name: "Alice" });
* const B = reactive({ name: "Bob", age: 42 });
* update(A, B);
* console.log(`${A.name} is ${A.age}`); // "Bob is 42"
*
* @example
* // Update a signal with assign()-like syntax:
* const s = signal(42);
* update(s, "hi"); // Argument type 'string' not assignable to type 'number'
* update(s, {}); // Argument type '{}' not assignable to type 'number'
* update(s, 43);
* console.log(s.value); // 43
*
* @param obj The Reactive or Signal to be updated
* @param update The value, Signal, object or Reactive to update `obj` to match
* @param overwrite If `true`, any properties `obj` missing from `update` are set to `undefined`
*/
/*
export function update<T extends SignalOrReactive>(
obj: T,
update: Partial<Unwrap<T>>,
overwrite = false
) {
if (obj instanceof Signal) {
obj.value = peekValue(update);
} else {
for (let i in update) {
if (i in obj) {
obj[i].value = peekValue(update[i]);
} else {
let sig = signal(peekValue(update[i]));
sig[KEY] = i;
obj[i] = sig;
}
}
if (overwrite) {
for (let i in obj) {
if (!(i in update)) {
obj[i].value = undefined;
}
}
}
const hooks =
currentComponent.__persistentState ||
(currentComponent.__persistentState = {
_list: [],
_pendingSetup: [],
});

if (index >= hooks._list.length) {
hooks._list.push({});
}

return hooks._list[index];
}

export function useStoreValueOnce<T>(factory: () => T): T {
const state = getState(currentHookIndex++);
if (!state._stored || (options as any)._skipEffects) {
state._stored = true;
state._stateValue = factory();
}
return state._stateValue;
}

export function useRef<T>(initialValue: T): { current: T } {
return useStoreValueOnce(() => ({ current: initialValue }));
}

function useOnce(callback: () => void | (() => void)): void {
const state = getState(currentHookIndex++);
if (!state._executed) {
state._executed = true;
state._stateValue = callback;
currentComponent!.__persistentState._pendingSetup.push(state);
}
}

function invokeEffect(hook: HookState): void {
console.log("invokeEffect", hook);
if (hook._stateValue) {
hook._cleanup = hook._stateValue() || undefined;
}
}

function invokeCleanup(hook: HookState): void {
if (hook._cleanup) {
hook._cleanup();
hook._cleanup = undefined;
}
}
*/
14 changes: 14 additions & 0 deletions packages/preact/src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ export interface AugmentedComponent extends Component<any, any> {
__v: VNode;
_updater?: Effect;
_updateFlags: number;
__persistentState: SignalState;
__P?: Element | Text | null;
}

export interface HookState {
_executed?: boolean;
_stored?: boolean;
_stateValue?: any;
_cleanup?: () => void;
}

export interface SignalState {
_list: HookState[];
_pendingSetup: HookState[];
}

export interface VNode<P = any> extends preact.VNode<P> {
Expand Down
6 changes: 3 additions & 3 deletions packages/preact/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ReadonlySignal, Signal } from "@preact/signals-core";
import { useSignal } from "@preact/signals";
import { useSignal, useStoreValueOnce } from "@preact/signals";
import { Fragment, createElement, JSX } from "preact";
import { useMemo } from "preact/hooks";

interface ShowProps<T = boolean> {
when: Signal<T> | ReadonlySignal<T>;
Expand All @@ -27,7 +26,7 @@ interface ForProps<T> {
}

export function For<T>(props: ForProps<T>): JSX.Element | null {
const cache = useMemo(() => new Map(), []);
const cache = useStoreValueOnce(() => new Map<T, JSX.Element>());
let list = (
(typeof props.each === "function" ? props.each() : props.each) as Signal<
Array<T>
Expand Down Expand Up @@ -60,6 +59,7 @@ export function useSignalRef<T>(value: T): Signal<T> & { current: T } {
Object.defineProperty(ref, "current", refSignalProto);
return ref;
}

const refSignalProto = {
configurable: true,
get(this: Signal) {
Expand Down