Skip to content

Commit 868ac8f

Browse files
committed
also fix React adapter
1 parent 6185290 commit 868ac8f

File tree

3 files changed

+149
-11
lines changed

3 files changed

+149
-11
lines changed

packages/preact/test/index.test.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,9 @@ describe("@preact/signals", () => {
375375
);
376376
}
377377

378-
render(<App />, scratch);
378+
act(() => {
379+
render(<App />, scratch);
380+
});
379381
expect(scratch.textContent).to.equal("foo");
380382
// expect(spy).not.to.have.been.called;
381383
await sleep(1);
@@ -387,8 +389,10 @@ describe("@preact/signals", () => {
387389

388390
spy.resetHistory();
389391

390-
sig.value = "bar";
391-
rerender();
392+
act(() => {
393+
sig.value = "bar";
394+
rerender();
395+
});
392396

393397
expect(scratch.textContent).to.equal("bar");
394398
await sleep(1);
@@ -436,8 +440,10 @@ describe("@preact/signals", () => {
436440
);
437441
spy.resetHistory();
438442

439-
sig.value = "bar";
440-
rerender();
443+
act(() => {
444+
sig.value = "bar";
445+
rerender();
446+
});
441447

442448
expect(scratch.textContent).to.equal("bar");
443449
await sleep(1);

packages/react/src/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export { signal, computed, batch, effect, Signal, type ReadonlySignal };
2424
const Empty = [] as const;
2525
const ReactElemType = Symbol.for("react.element"); // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L15
2626
const ReactMemoType = Symbol.for("react.memo"); // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L30
27-
const ProxyInstance = new WeakMap<FunctionComponent<any>, FunctionComponent<any>>();
27+
const ProxyInstance = new WeakMap<
28+
FunctionComponent<any>,
29+
FunctionComponent<any>
30+
>();
2831
const SupportsProxy = typeof Proxy === "function";
2932

3033
const ProxyHandlers = {
@@ -229,8 +232,6 @@ export function useSignalEffect(cb: () => void | (() => void)) {
229232
callback.current = cb;
230233

231234
useEffect(() => {
232-
return effect(() => {
233-
return callback.current();
234-
});
235+
return effect(() => callback.current());
235236
}, Empty);
236237
}

packages/react/test/index.test.tsx

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @ts-ignore-next-line
22
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
33

4-
import { signal, useComputed } from "@preact/signals-react";
5-
import { createElement, useMemo, memo, StrictMode } from "react";
4+
import { signal, useComputed, useSignalEffect } from "@preact/signals-react";
5+
import { createElement, useMemo, memo, StrictMode, createRef } from "react";
66
import { createRoot, Root } from "react-dom/client";
77
import { renderToStaticMarkup } from "react-dom/server";
88
import { act } from "react-dom/test-utils";
@@ -222,4 +222,135 @@ describe("@preact/signals-react", () => {
222222
}
223223
});
224224
});
225+
226+
describe("useSignalEffect()", () => {
227+
it("should be invoked after commit", async () => {
228+
const ref = createRef<HTMLDivElement>();
229+
const sig = signal("foo");
230+
const spy = sinon.spy();
231+
let count = 0;
232+
233+
function App() {
234+
useSignalEffect(() =>
235+
spy(
236+
sig.value,
237+
ref.current,
238+
ref.current!.getAttribute("data-render-id")
239+
)
240+
);
241+
return (
242+
<p ref={ref} data-render-id={count++}>
243+
{sig.value}
244+
</p>
245+
);
246+
}
247+
248+
render(<App />);
249+
expect(scratch.textContent).to.equal("foo");
250+
251+
expect(spy).to.have.been.calledOnceWith(
252+
"foo",
253+
scratch.firstElementChild,
254+
"0"
255+
);
256+
257+
spy.resetHistory();
258+
259+
act(() => {
260+
sig.value = "bar";
261+
});
262+
263+
expect(scratch.textContent).to.equal("bar");
264+
265+
// NOTE: Ideally, call should receive "1" as its third argument!
266+
// The "0" indicates that Preact's DOM mutations hadn't yet been performed when the callback ran.
267+
// This happens because we do signal-based effect runs after the first, not VDOM.
268+
// Perhaps we could find a way to defer the callback when it coincides with a render?
269+
expect(spy).to.have.been.calledOnceWith(
270+
"bar",
271+
scratch.firstElementChild,
272+
"0" // ideally "1" - update if we find a nice way to do so!
273+
);
274+
});
275+
276+
it("should invoke any returned cleanup function for updates", async () => {
277+
const ref = createRef<HTMLDivElement>();
278+
const sig = signal("foo");
279+
const spy = sinon.spy();
280+
const cleanup = sinon.spy();
281+
let count = 0;
282+
283+
function App() {
284+
useSignalEffect(() => {
285+
const id = ref.current!.getAttribute("data-render-id");
286+
const value = sig.value;
287+
spy(value, ref.current, id);
288+
return () => cleanup(value, ref.current, id);
289+
});
290+
return (
291+
<p ref={ref} data-render-id={count++}>
292+
{sig.value}
293+
</p>
294+
);
295+
}
296+
297+
render(<App />);
298+
299+
expect(cleanup).not.to.have.been.called;
300+
expect(spy).to.have.been.calledOnceWith(
301+
"foo",
302+
scratch.firstElementChild,
303+
"0"
304+
);
305+
spy.resetHistory();
306+
307+
act(() => {
308+
sig.value = "bar";
309+
});
310+
311+
expect(scratch.textContent).to.equal("bar");
312+
313+
const child = scratch.firstElementChild;
314+
315+
expect(cleanup).to.have.been.calledOnceWith("foo", child, "0");
316+
317+
expect(spy).to.have.been.calledOnceWith(
318+
"bar",
319+
child,
320+
"0" // ideally "1" - update if we find a nice way to do so!
321+
);
322+
});
323+
324+
it("should invoke any returned cleanup function for unmounts", async () => {
325+
const ref = createRef<HTMLDivElement>();
326+
const sig = signal("foo");
327+
const spy = sinon.spy();
328+
const cleanup = sinon.spy();
329+
330+
function App() {
331+
useSignalEffect(() => {
332+
const value = sig.value;
333+
spy(value, ref.current);
334+
return () => cleanup(value, ref.current);
335+
});
336+
return <p ref={ref}>{sig.value}</p>;
337+
}
338+
339+
render(<App />);
340+
341+
const child = scratch.firstElementChild;
342+
343+
expect(cleanup).not.to.have.been.called;
344+
expect(spy).to.have.been.calledOnceWith("foo", child);
345+
spy.resetHistory();
346+
347+
render(null);
348+
349+
expect(spy).not.to.have.been.called;
350+
expect(cleanup).to.have.been.calledOnce;
351+
// @note: React cleans up the ref eagerly, so it's already null by the time the callback runs.
352+
// this is probably worth fixing at some point.
353+
expect(cleanup).to.have.been.calledWith("foo", null);
354+
});
355+
});
225356
});

0 commit comments

Comments
 (0)