@preact/signals-react 1.3.4 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,422 @@
1
+ import { createElement, Fragment } from "react";
2
+ import { Signal, signal, batch } from "@preact/signals-core";
3
+ import { useSignals } from "@preact/signals-react/runtime";
4
+ import {
5
+ Root,
6
+ createRoot,
7
+ act,
8
+ checkHangingAct,
9
+ getConsoleErrorSpy,
10
+ checkConsoleErrorLogs,
11
+ } from "../../test/shared/utils";
12
+
13
+ describe("useSignals", () => {
14
+ let scratch: HTMLDivElement;
15
+ let root: Root;
16
+
17
+ async function render(element: Parameters<Root["render"]>[0]) {
18
+ await act(() => root.render(element));
19
+ }
20
+
21
+ beforeEach(async () => {
22
+ scratch = document.createElement("div");
23
+ document.body.appendChild(scratch);
24
+ root = await createRoot(scratch);
25
+ getConsoleErrorSpy().resetHistory();
26
+ });
27
+
28
+ afterEach(async () => {
29
+ await act(() => root.unmount());
30
+ scratch.remove();
31
+
32
+ checkConsoleErrorLogs();
33
+ checkHangingAct();
34
+ });
35
+
36
+ it("should rerender components when signals they use change", async () => {
37
+ const signal1 = signal(0);
38
+ function Child1() {
39
+ useSignals();
40
+ return <p>{signal1.value}</p>;
41
+ }
42
+
43
+ const signal2 = signal(0);
44
+ function Child2() {
45
+ useSignals();
46
+ return <p>{signal2.value}</p>;
47
+ }
48
+
49
+ function Parent() {
50
+ return (
51
+ <Fragment>
52
+ <Child1 />
53
+ <Child2 />
54
+ </Fragment>
55
+ );
56
+ }
57
+
58
+ await render(<Parent />);
59
+ expect(scratch.innerHTML).to.equal("<p>0</p><p>0</p>");
60
+
61
+ await act(() => {
62
+ signal1.value += 1;
63
+ });
64
+ expect(scratch.innerHTML).to.equal("<p>1</p><p>0</p>");
65
+
66
+ await act(() => {
67
+ signal2.value += 1;
68
+ });
69
+ expect(scratch.innerHTML).to.equal("<p>1</p><p>1</p>");
70
+ });
71
+
72
+ it("should correctly invoke rerenders if useSignals is called multiple times in the same component", async () => {
73
+ const signal1 = signal(0);
74
+ const signal2 = signal(0);
75
+ const signal3 = signal(0);
76
+ function App() {
77
+ useSignals();
78
+ const sig1 = signal1.value;
79
+ useSignals();
80
+ const sig2 = signal2.value;
81
+ const sig3 = signal3.value;
82
+ useSignals();
83
+ return (
84
+ <p>
85
+ {sig1}
86
+ {sig2}
87
+ {sig3}
88
+ </p>
89
+ );
90
+ }
91
+
92
+ await render(<App />);
93
+ expect(scratch.innerHTML).to.equal("<p>000</p>");
94
+
95
+ await act(() => {
96
+ signal1.value += 1;
97
+ });
98
+ expect(scratch.innerHTML).to.equal("<p>100</p>");
99
+
100
+ await act(() => {
101
+ signal2.value += 1;
102
+ });
103
+ expect(scratch.innerHTML).to.equal("<p>110</p>");
104
+
105
+ await act(() => {
106
+ signal3.value += 1;
107
+ });
108
+ expect(scratch.innerHTML).to.equal("<p>111</p>");
109
+ });
110
+
111
+ it("should not rerender components when signals they use do not change", async () => {
112
+ const child1Spy = sinon.spy();
113
+ const signal1 = signal(0);
114
+ function Child1() {
115
+ child1Spy();
116
+ useSignals();
117
+ return <p>{signal1.value}</p>;
118
+ }
119
+
120
+ const child2Spy = sinon.spy();
121
+ const signal2 = signal(0);
122
+ function Child2() {
123
+ child2Spy();
124
+ useSignals();
125
+ return <p>{signal2.value}</p>;
126
+ }
127
+
128
+ const parentSpy = sinon.spy();
129
+ function Parent() {
130
+ parentSpy();
131
+ return (
132
+ <Fragment>
133
+ <Child1 />
134
+ <Child2 />
135
+ </Fragment>
136
+ );
137
+ }
138
+
139
+ function resetSpies() {
140
+ child1Spy.resetHistory();
141
+ child2Spy.resetHistory();
142
+ parentSpy.resetHistory();
143
+ }
144
+
145
+ resetSpies();
146
+ await render(<Parent />);
147
+ expect(scratch.innerHTML).to.equal("<p>0</p><p>0</p>");
148
+ expect(child1Spy).to.have.been.calledOnce;
149
+ expect(child2Spy).to.have.been.calledOnce;
150
+ expect(parentSpy).to.have.been.calledOnce;
151
+
152
+ resetSpies();
153
+ await act(() => {
154
+ signal1.value += 1;
155
+ });
156
+ expect(scratch.innerHTML).to.equal("<p>1</p><p>0</p>");
157
+ expect(child1Spy).to.have.been.calledOnce;
158
+ expect(child2Spy).to.not.have.been.called;
159
+ expect(parentSpy).to.not.have.been.called;
160
+
161
+ resetSpies();
162
+ await act(() => {
163
+ signal2.value += 1;
164
+ });
165
+ expect(scratch.innerHTML).to.equal("<p>1</p><p>1</p>");
166
+ expect(child1Spy).to.not.have.been.called;
167
+ expect(child2Spy).to.have.been.calledOnce;
168
+ expect(parentSpy).to.not.have.been.called;
169
+ });
170
+
171
+ it("should not rerender components when signals they use change but they are not mounted", async () => {
172
+ const child1Spy = sinon.spy();
173
+ const signal1 = signal(0);
174
+ function Child() {
175
+ child1Spy();
176
+ useSignals();
177
+ const sig1 = signal1.value;
178
+ return <p>{sig1}</p>;
179
+ }
180
+
181
+ function Parent({ show }: { show: boolean }) {
182
+ return <Fragment>{show && <Child />}</Fragment>;
183
+ }
184
+
185
+ await render(<Parent show={true} />);
186
+ expect(scratch.innerHTML).to.equal("<p>0</p>");
187
+
188
+ await act(() => {
189
+ signal1.value += 1;
190
+ });
191
+ expect(scratch.innerHTML).to.equal("<p>1</p>");
192
+
193
+ await act(() => {
194
+ render(<Parent show={false} />);
195
+ });
196
+ expect(scratch.innerHTML).to.equal("");
197
+
198
+ await act(() => {
199
+ signal1.value += 1;
200
+ });
201
+ expect(child1Spy).to.have.been.calledTwice;
202
+ });
203
+
204
+ it("should not rerender components that only update signals in event handlers", async () => {
205
+ const buttonSpy = sinon.spy();
206
+ function AddOneButton({ num }: { num: Signal<number> }) {
207
+ useSignals();
208
+ buttonSpy();
209
+ return (
210
+ <button
211
+ onClick={() => {
212
+ num.value += 1;
213
+ }}
214
+ >
215
+ Add One
216
+ </button>
217
+ );
218
+ }
219
+
220
+ const displaySpy = sinon.spy();
221
+ function DisplayNumber({ num }: { num: Signal<number> }) {
222
+ useSignals();
223
+ displaySpy();
224
+ return <p>{num.value}</p>;
225
+ }
226
+
227
+ const number = signal(0);
228
+ function App() {
229
+ return (
230
+ <Fragment>
231
+ <AddOneButton num={number} />
232
+ <DisplayNumber num={number} />
233
+ </Fragment>
234
+ );
235
+ }
236
+
237
+ await render(<App />);
238
+ expect(scratch.innerHTML).to.equal("<button>Add One</button><p>0</p>");
239
+ expect(buttonSpy).to.have.been.calledOnce;
240
+ expect(displaySpy).to.have.been.calledOnce;
241
+
242
+ await act(() => {
243
+ scratch.querySelector("button")!.click();
244
+ });
245
+
246
+ expect(scratch.innerHTML).to.equal("<button>Add One</button><p>1</p>");
247
+ expect(buttonSpy).to.have.been.calledOnce;
248
+ expect(displaySpy).to.have.been.calledTwice;
249
+ });
250
+
251
+ it("should not rerender components that only read signals in event handlers", async () => {
252
+ const buttonSpy = sinon.spy();
253
+ function AddOneButton({ num }: { num: Signal<number> }) {
254
+ useSignals();
255
+ buttonSpy();
256
+ return (
257
+ <button
258
+ onClick={() => {
259
+ num.value += adder.value;
260
+ }}
261
+ >
262
+ Add One
263
+ </button>
264
+ );
265
+ }
266
+
267
+ const displaySpy = sinon.spy();
268
+ function DisplayNumber({ num }: { num: Signal<number> }) {
269
+ useSignals();
270
+ displaySpy();
271
+ return <p>{num.value}</p>;
272
+ }
273
+
274
+ const adder = signal(2);
275
+ const number = signal(0);
276
+ function App() {
277
+ return (
278
+ <Fragment>
279
+ <AddOneButton num={number} />
280
+ <DisplayNumber num={number} />
281
+ </Fragment>
282
+ );
283
+ }
284
+
285
+ function resetSpies() {
286
+ buttonSpy.resetHistory();
287
+ displaySpy.resetHistory();
288
+ }
289
+
290
+ resetSpies();
291
+ await render(<App />);
292
+ expect(scratch.innerHTML).to.equal("<button>Add One</button><p>0</p>");
293
+ expect(buttonSpy).to.have.been.calledOnce;
294
+ expect(displaySpy).to.have.been.calledOnce;
295
+
296
+ resetSpies();
297
+ await act(() => {
298
+ scratch.querySelector("button")!.click();
299
+ });
300
+
301
+ expect(scratch.innerHTML).to.equal("<button>Add One</button><p>2</p>");
302
+ expect(buttonSpy).to.not.have.been.called;
303
+ expect(displaySpy).to.have.been.calledOnce;
304
+
305
+ resetSpies();
306
+ await act(() => {
307
+ adder.value += 1;
308
+ });
309
+
310
+ expect(scratch.innerHTML).to.equal("<button>Add One</button><p>2</p>");
311
+ expect(buttonSpy).to.not.have.been.called;
312
+ expect(displaySpy).to.not.have.been.called;
313
+
314
+ resetSpies();
315
+ await act(() => {
316
+ scratch.querySelector("button")!.click();
317
+ });
318
+
319
+ expect(scratch.innerHTML).to.equal("<button>Add One</button><p>5</p>");
320
+ expect(buttonSpy).to.not.have.been.called;
321
+ expect(displaySpy).to.have.been.calledOnce;
322
+ });
323
+
324
+ it("should properly rerender components that use custom hooks", async () => {
325
+ const greeting = signal("Hello");
326
+ function useGreeting() {
327
+ useSignals();
328
+ return greeting.value;
329
+ }
330
+
331
+ const name = signal("John");
332
+ function useName() {
333
+ useSignals();
334
+ return name.value;
335
+ }
336
+
337
+ function App() {
338
+ const greeting = useGreeting();
339
+ const name = useName();
340
+ return (
341
+ <div>
342
+ {greeting} {name}!
343
+ </div>
344
+ );
345
+ }
346
+
347
+ await render(<App />);
348
+ expect(scratch.innerHTML).to.equal("<div>Hello John!</div>");
349
+
350
+ await act(() => {
351
+ greeting.value = "Hi";
352
+ });
353
+ expect(scratch.innerHTML).to.equal("<div>Hi John!</div>");
354
+
355
+ await act(() => {
356
+ name.value = "Jane";
357
+ });
358
+ expect(scratch.innerHTML).to.equal("<div>Hi Jane!</div>");
359
+
360
+ await act(() => {
361
+ batch(() => {
362
+ greeting.value = "Hello";
363
+ name.value = "John";
364
+ });
365
+ });
366
+ expect(scratch.innerHTML).to.equal("<div>Hello John!</div>");
367
+ });
368
+
369
+ it("should properly rerender components that use custom hooks and signals", async () => {
370
+ const greeting = signal("Hello");
371
+ function useGreeting() {
372
+ useSignals();
373
+ return greeting.value;
374
+ }
375
+
376
+ const name = signal("John");
377
+ function useName() {
378
+ useSignals();
379
+ return name.value;
380
+ }
381
+
382
+ const punctuation = signal("!");
383
+ function App() {
384
+ useSignals();
385
+ const greeting = useGreeting();
386
+ const name = useName();
387
+ return (
388
+ <div>
389
+ {greeting} {name}
390
+ {punctuation.value}
391
+ </div>
392
+ );
393
+ }
394
+
395
+ await render(<App />);
396
+ expect(scratch.innerHTML).to.equal("<div>Hello John!</div>");
397
+
398
+ await act(() => {
399
+ greeting.value = "Hi";
400
+ });
401
+ expect(scratch.innerHTML).to.equal("<div>Hi John!</div>");
402
+
403
+ await act(() => {
404
+ name.value = "Jane";
405
+ });
406
+ expect(scratch.innerHTML).to.equal("<div>Hi Jane!</div>");
407
+
408
+ await act(() => {
409
+ punctuation.value = "?";
410
+ });
411
+ expect(scratch.innerHTML).to.equal("<div>Hi Jane?</div>");
412
+
413
+ await act(() => {
414
+ batch(() => {
415
+ greeting.value = "Hello";
416
+ name.value = "John";
417
+ punctuation.value = "!";
418
+ });
419
+ });
420
+ expect(scratch.innerHTML).to.equal("<div>Hello John!</div>");
421
+ });
422
+ });
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  effect,
6
6
  Signal,
7
7
  type ReadonlySignal,
8
+ untracked,
8
9
  } from "@preact/signals-core";
9
10
  import type { ReactElement } from "react";
10
11
  import { useSignal, useComputed, useSignalEffect } from "../runtime";
@@ -20,6 +21,7 @@ export {
20
21
  useSignal,
21
22
  useComputed,
22
23
  useSignalEffect,
24
+ untracked,
23
25
  };
24
26
 
25
27
  declare module "@preact/signals-core" {
@@ -11,6 +11,7 @@ import {
11
11
  } from "@preact/signals-react";
12
12
  import {
13
13
  createElement,
14
+ Fragment,
14
15
  forwardRef,
15
16
  useMemo,
16
17
  useReducer,
@@ -57,7 +58,7 @@ describe("@preact/signals-react updating", () => {
57
58
  checkHangingAct();
58
59
  });
59
60
 
60
- describe("Text bindings", () => {
61
+ describe("SignalValue bindings", () => {
61
62
  it("should render text without signals", async () => {
62
63
  await render(<span>test</span>);
63
64
  const span = scratch.firstChild;
@@ -65,7 +66,7 @@ describe("@preact/signals-react updating", () => {
65
66
  expect(text).to.have.property("data", "test");
66
67
  });
67
68
 
68
- it("should render Signals as Text", async () => {
69
+ it("should render Signals as SignalValue", async () => {
69
70
  const sig = signal("test");
70
71
  await render(<span>{sig}</span>);
71
72
  const span = scratch.firstChild;
@@ -74,7 +75,7 @@ describe("@preact/signals-react updating", () => {
74
75
  expect(text).to.have.property("data", "test");
75
76
  });
76
77
 
77
- it("should render computed as Text", async () => {
78
+ it("should render computed as SignalValue", async () => {
78
79
  const sig = signal("test");
79
80
  const comp = computed(() => `${sig} ${sig}`);
80
81
  await render(<span>{comp}</span>);
@@ -84,7 +85,7 @@ describe("@preact/signals-react updating", () => {
84
85
  expect(text).to.have.property("data", "test test");
85
86
  });
86
87
 
87
- it("should update Signal-based Text (no parent component)", async () => {
88
+ it("should update Signal-based SignalValue (no parent component)", async () => {
88
89
  const sig = signal("test");
89
90
  await render(<span>{sig}</span>);
90
91
 
@@ -95,13 +96,13 @@ describe("@preact/signals-react updating", () => {
95
96
  sig.value = "changed";
96
97
  });
97
98
 
98
- // should not remount/replace Text
99
+ // should not remount/replace SignalValue
99
100
  expect(scratch.firstChild!.firstChild!).to.equal(text);
100
101
  // should update the text in-place
101
102
  expect(text).to.have.property("data", "changed");
102
103
  });
103
104
 
104
- it("should update Signal-based Text (in a parent component)", async () => {
105
+ it("should update Signal-based SignalValue (in a parent component)", async () => {
105
106
  const sig = signal("test");
106
107
  function App({ x }: { x: typeof sig }) {
107
108
  return <span>{x}</span>;
@@ -115,11 +116,31 @@ describe("@preact/signals-react updating", () => {
115
116
  sig.value = "changed";
116
117
  });
117
118
 
118
- // should not remount/replace Text
119
+ // should not remount/replace SignalValue
119
120
  expect(scratch.firstChild!.firstChild!).to.equal(text);
120
121
  // should update the text in-place
121
122
  expect(text).to.have.property("data", "changed");
122
123
  });
124
+
125
+ it("should work with JSX inside signal", async () => {
126
+ const sig = signal(<b>test</b>);
127
+ function App({ x }: { x: typeof sig }) {
128
+ return <span>{x}</span>;
129
+ }
130
+ await render(<App x={sig} />);
131
+
132
+ let text = scratch.firstChild!.firstChild!;
133
+ expect(text).to.be.instanceOf(HTMLElement);
134
+ expect(text.firstChild).to.have.property("data", "test");
135
+
136
+ await act(() => {
137
+ sig.value = <div>changed</div>;
138
+ });
139
+
140
+ text = scratch.firstChild!.firstChild!;
141
+ expect(text).to.be.instanceOf(HTMLDivElement);
142
+ expect(text.firstChild).to.have.property("data", "changed");
143
+ });
123
144
  });
124
145
 
125
146
  describe("Component bindings", () => {
@@ -140,6 +161,40 @@ describe("@preact/signals-react updating", () => {
140
161
  expect(scratch.textContent).to.equal("bar");
141
162
  });
142
163
 
164
+ it("should rerender components when signals they use change", async () => {
165
+ const signal1 = signal(0);
166
+ function Child1() {
167
+ return <div>{signal1}</div>;
168
+ }
169
+
170
+ const signal2 = signal(0);
171
+ function Child2() {
172
+ return <div>{signal2}</div>;
173
+ }
174
+
175
+ function Parent() {
176
+ return (
177
+ <Fragment>
178
+ <Child1 />
179
+ <Child2 />
180
+ </Fragment>
181
+ );
182
+ }
183
+
184
+ await render(<Parent />);
185
+ expect(scratch.innerHTML).to.equal("<div>0</div><div>0</div>");
186
+
187
+ await act(() => {
188
+ signal1.value += 1;
189
+ });
190
+ expect(scratch.innerHTML).to.equal("<div>1</div><div>0</div>");
191
+
192
+ await act(() => {
193
+ signal2.value += 1;
194
+ });
195
+ expect(scratch.innerHTML).to.equal("<div>1</div><div>1</div>");
196
+ });
197
+
143
198
  it("should subscribe to signals passed as props to DOM elements", async () => {
144
199
  const className = signal("foo");
145
200
  function App() {
@@ -29,13 +29,13 @@ export function mountSignalsTests(
29
29
  expect(html).to.equal("<span>test</span>");
30
30
  });
31
31
 
32
- it("should render Signals as Text", async () => {
32
+ it("should render Signals as SignalValue", async () => {
33
33
  const sig = signal("test");
34
34
  const html = await render(<span>{sig}</span>);
35
35
  expect(html).to.equal("<span>test</span>");
36
36
  });
37
37
 
38
- it("should render computed as Text", async () => {
38
+ it("should render computed as SignalValue", async () => {
39
39
  const sig = signal("test");
40
40
  const comp = computed(() => `${sig} ${sig}`);
41
41
  const html = await render(<span>{comp}</span>);