Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .changeset/mighty-ghosts-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@preact/signals": minor
---

Ensure that a state settled hook cant interfere with a signal update, this is in
preparation for the Preact v11 release.
37 changes: 34 additions & 3 deletions packages/preact/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export {
const HAS_PENDING_UPDATE = 1 << 0;
const HAS_HOOK_STATE = 1 << 1;
const HAS_COMPUTEDS = 1 << 2;
const SHOULD_UPDATE = 1 << 4;

const PREACT_SKIP_CHILDREN = 1 << 3;

let oldNotify: (this: Effect) => void,
effectsQueue: Array<Effect> = [],
Expand Down Expand Up @@ -355,15 +358,24 @@ Component.prototype.shouldComponentUpdate = function (
// @ts-ignore
for (let i in state) return true;

const hasHooksState = this._updateFlags & HAS_HOOK_STATE;
if (this.__f || (typeof this.u == "boolean" && this.u === true)) {
const hasHooksState = this._updateFlags & HAS_HOOK_STATE;
// if this component used no signals or computeds and no hooks state, update:
if (!hasSignals && !hasHooksState && !(this._updateFlags & HAS_COMPUTEDS))
if (!hasSignals && !hasHooksState && !(this._updateFlags & HAS_COMPUTEDS)) {
if (hasHooksState) {
this._updateFlags |= SHOULD_UPDATE;
}
return true;
}

// if there is a pending re-render triggered from Signals,
// or if there is hooks state, update:
if (this._updateFlags & HAS_PENDING_UPDATE) return true;
if (this._updateFlags & HAS_PENDING_UPDATE) {
if (hasHooksState) {
this._updateFlags |= SHOULD_UPDATE;
}
return true;
}
} else {
// if this component used no signals or computeds, update:
if (!hasSignals && !(this._updateFlags & HAS_COMPUTEDS)) return true;
Expand All @@ -383,6 +395,25 @@ Component.prototype.shouldComponentUpdate = function (
return false;
};

hook(OptionsTypes.AFTER_RENDER, (old, vnode: VNode) => {
const component = vnode.__c;
if (component && component._updateFlags & SHOULD_UPDATE) {
// When we have a signal that should update and we see that SKIP_CHILDREN is set,
// we need to clear it so that the children are rendered.
// This is a rare scenario where we both have a signal update as well as
// a hook that updates and settles on the same value.
component._updateFlags &= ~SHOULD_UPDATE;
// @ts-ignore
if (vnode.__u & PREACT_SKIP_CHILDREN) {
// @ts-ignore
vnode.__u &= ~PREACT_SKIP_CHILDREN;
}
}
// This is a Preact 11 only hook and is meant to take care of state-settling in hooks.
// Explained in https://github.com/preactjs/preact/pull/4760
old(vnode);
});

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>) {
Expand Down
2 changes: 2 additions & 0 deletions packages/preact/src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ export const enum OptionsTypes {
RENDER = "__r",
CATCH_ERROR = "__e",
UNMOUNT = "unmount",
AFTER_RENDER = "__d",
}

export interface OptionsType {
[OptionsTypes.HOOK](component: Component, index: number, type: number): void;
[OptionsTypes.DIFF](vnode: VNode): void;
[OptionsTypes.DIFFED](vnode: VNode): void;
[OptionsTypes.RENDER](vnode: VNode): void;
[OptionsTypes.AFTER_RENDER](vnode: VNode): void;
[OptionsTypes.CATCH_ERROR](error: any, vnode: VNode, oldVNode: VNode): void;
[OptionsTypes.UNMOUNT](vnode: VNode): void;
}
Expand Down
28 changes: 28 additions & 0 deletions packages/preact/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,34 @@ describe("@preact/signals", () => {
});
expect(scratch.innerHTML).to.equal("<p>bar baz</p>");
});

it("state settling should not prevent a signal update from being rendered", () => {
let set: any;
const test2 = signal("Foo");
function Test() {
const [test, setTest] = useState("foo");
set = setTest;
return (
<p>
{test} {test2.value}
</p>
);
}

function App() {
return <Test />;
}

render(<App />, scratch);

expect(scratch.innerHTML).to.equal("<p>foo Foo</p>");
act(() => {
set("bar");
test2.value = "Bar";
set("foo");
});
expect(scratch.innerHTML).to.equal("<p>foo Bar</p>");
});
});

describe("useSignalEffect()", () => {
Expand Down