@hoosk/minisignals-react 1.0.4 → 1.0.6
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.
- package/README.md +17 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/package.json +12 -3
- package/tests/adapter.test.tsx +140 -20
package/README.md
CHANGED
|
@@ -31,4 +31,21 @@ function Counter() {
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
### `useSignalValue` accepts any `ReadonlySignal`
|
|
35
|
+
|
|
36
|
+
`useSignalValue` accepts both `Signal<T>` and `Computed<T>` via the shared `ReadonlySignal<T>` interface, so you can pass either directly:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { signal, computed } from '@hoosk/minisignals';
|
|
40
|
+
import { useSignalValue } from '@hoosk/minisignals-react';
|
|
41
|
+
|
|
42
|
+
const price = signal(100);
|
|
43
|
+
const doubled = computed(() => price.value * 2);
|
|
44
|
+
|
|
45
|
+
function Price() {
|
|
46
|
+
const value = useSignalValue(doubled); // works with Computed too
|
|
47
|
+
return <p>{value}</p>;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
34
51
|
For full documentation, please refer to the [main repository README](../../README.md).
|
package/dist/index.d.cts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReadonlySignal, Signal } from '@hoosk/minisignals';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A hook that subscribes to a signal or computed value and triggers
|
|
5
5
|
* a React re-render whenever the value changes.
|
|
6
6
|
* Uses React 18's useSyncExternalStore for tear-free concurrent rendering.
|
|
7
7
|
*
|
|
8
|
-
* @param sig
|
|
8
|
+
* @param sig Any ReadonlySignal (Signal or Computed) to observe.
|
|
9
9
|
* @returns The current value of the signal.
|
|
10
10
|
*/
|
|
11
|
-
declare function useSignalValue<T>(sig:
|
|
11
|
+
declare function useSignalValue<T>(sig: ReadonlySignal<T>): T;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Creates a signal that is tied to the component's lifecycle.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReadonlySignal, Signal } from '@hoosk/minisignals';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A hook that subscribes to a signal or computed value and triggers
|
|
5
5
|
* a React re-render whenever the value changes.
|
|
6
6
|
* Uses React 18's useSyncExternalStore for tear-free concurrent rendering.
|
|
7
7
|
*
|
|
8
|
-
* @param sig
|
|
8
|
+
* @param sig Any ReadonlySignal (Signal or Computed) to observe.
|
|
9
9
|
* @returns The current value of the signal.
|
|
10
10
|
*/
|
|
11
|
-
declare function useSignalValue<T>(sig:
|
|
11
|
+
declare function useSignalValue<T>(sig: ReadonlySignal<T>): T;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Creates a signal that is tied to the component's lifecycle.
|
package/package.json
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoosk/minisignals-react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "React adapter for @hoosk/minisignals",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.
|
|
7
|
-
"module": "dist/index.
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
9
17
|
"peerDependencies": {
|
|
10
18
|
"react": ">=18.0.0"
|
|
11
19
|
},
|
|
@@ -24,6 +32,7 @@
|
|
|
24
32
|
"scripts": {
|
|
25
33
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
26
34
|
"dev": "tsup src/index.ts --format cjs,esm --watch --dts",
|
|
35
|
+
"prepare": "npm run build",
|
|
27
36
|
"test": "vitest run"
|
|
28
37
|
},
|
|
29
38
|
"author": "Hoosk",
|
package/tests/adapter.test.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
-
import { render, screen, act } from '@testing-library/react';
|
|
4
|
-
import { signal, computed } from '@hoosk/minisignals';
|
|
3
|
+
import { render, screen, act, cleanup } from '@testing-library/react';
|
|
4
|
+
import { signal, computed, batch } from '@hoosk/minisignals';
|
|
5
5
|
import { useSignalValue, useSignal } from '../src/index.ts';
|
|
6
6
|
|
|
7
7
|
describe('@hoosk/minisignals-react adapter', () => {
|
|
8
8
|
describe('useSignalValue()', () => {
|
|
9
9
|
it('should render the initial value of a signal', () => {
|
|
10
10
|
const count = signal(5);
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
const TestComponent = () => {
|
|
13
13
|
const val = useSignalValue(count);
|
|
14
14
|
return <div>Value: {val}</div>;
|
|
@@ -20,7 +20,7 @@ describe('@hoosk/minisignals-react adapter', () => {
|
|
|
20
20
|
|
|
21
21
|
it('should trigger a re-render when the signal mutates', () => {
|
|
22
22
|
const count = signal(10);
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
const TestComponent = () => {
|
|
25
25
|
const val = useSignalValue(count);
|
|
26
26
|
return <div data-testid="display">{val}</div>;
|
|
@@ -30,9 +30,7 @@ describe('@hoosk/minisignals-react adapter', () => {
|
|
|
30
30
|
const display = screen.getByTestId('display');
|
|
31
31
|
expect(display.textContent).toBe('10');
|
|
32
32
|
|
|
33
|
-
act(() => {
|
|
34
|
-
count.value = 20;
|
|
35
|
-
});
|
|
33
|
+
act(() => { count.value = 20; });
|
|
36
34
|
|
|
37
35
|
expect(display.textContent).toBe('20');
|
|
38
36
|
});
|
|
@@ -40,7 +38,7 @@ describe('@hoosk/minisignals-react adapter', () => {
|
|
|
40
38
|
it('should work seamlessly with computed signals', () => {
|
|
41
39
|
const name = signal('A');
|
|
42
40
|
const doubledName = computed(() => name.value + name.value);
|
|
43
|
-
|
|
41
|
+
|
|
44
42
|
const TestComponent = () => {
|
|
45
43
|
const val = useSignalValue(doubledName);
|
|
46
44
|
return <div data-testid="computed-display">{val}</div>;
|
|
@@ -50,25 +48,115 @@ describe('@hoosk/minisignals-react adapter', () => {
|
|
|
50
48
|
const display = screen.getByTestId('computed-display');
|
|
51
49
|
expect(display.textContent).toBe('AA');
|
|
52
50
|
|
|
51
|
+
act(() => { name.value = 'B'; });
|
|
52
|
+
|
|
53
|
+
expect(display.textContent).toBe('BB');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('NEGATIVE CASE: should unsubscribe when the component unmounts (no memory leak)', () => {
|
|
57
|
+
const count = signal(0);
|
|
58
|
+
const renderSpy = vi.fn();
|
|
59
|
+
|
|
60
|
+
const TestComponent = () => {
|
|
61
|
+
renderSpy();
|
|
62
|
+
const val = useSignalValue(count);
|
|
63
|
+
return <div>{val}</div>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const { unmount } = render(<TestComponent />);
|
|
67
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
68
|
+
|
|
69
|
+
unmount();
|
|
70
|
+
|
|
71
|
+
// Mutating after unmount must NOT trigger any render
|
|
72
|
+
act(() => { count.value = 99; });
|
|
73
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should re-subscribe when the observed signal instance changes', () => {
|
|
77
|
+
const sigA = signal('A');
|
|
78
|
+
const sigB = signal('B');
|
|
79
|
+
|
|
80
|
+
const TestComponent = ({ which }: { which: 'a' | 'b' }) => {
|
|
81
|
+
const val = useSignalValue(which === 'a' ? sigA : sigB);
|
|
82
|
+
return <div data-testid="out">{val}</div>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const { rerender } = render(<TestComponent which="a" />);
|
|
86
|
+
expect(screen.getByTestId('out').textContent).toBe('A');
|
|
87
|
+
|
|
88
|
+
rerender(<TestComponent which="b" />);
|
|
89
|
+
expect(screen.getByTestId('out').textContent).toBe('B');
|
|
90
|
+
|
|
91
|
+
// Mutating the old signal must not re-render
|
|
92
|
+
act(() => { sigA.value = 'A2'; });
|
|
93
|
+
expect(screen.getByTestId('out').textContent).toBe('B');
|
|
94
|
+
|
|
95
|
+
// Mutating the new signal must re-render
|
|
96
|
+
act(() => { sigB.value = 'B2'; });
|
|
97
|
+
expect(screen.getByTestId('out').textContent).toBe('B2');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should update multiple sibling components sharing the same signal independently', () => {
|
|
101
|
+
const shared = signal(0);
|
|
102
|
+
const renderCounts = { a: 0, b: 0 };
|
|
103
|
+
|
|
104
|
+
const CompA = () => {
|
|
105
|
+
renderCounts.a++;
|
|
106
|
+
return <span data-testid="a">{useSignalValue(shared)}</span>;
|
|
107
|
+
};
|
|
108
|
+
const CompB = () => {
|
|
109
|
+
renderCounts.b++;
|
|
110
|
+
return <span data-testid="b">{useSignalValue(shared)}</span>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
render(<><CompA /><CompB /></>);
|
|
114
|
+
expect(renderCounts).toEqual({ a: 1, b: 1 });
|
|
115
|
+
|
|
116
|
+
act(() => { shared.value = 42; });
|
|
117
|
+
|
|
118
|
+
expect(screen.getByTestId('a').textContent).toBe('42');
|
|
119
|
+
expect(screen.getByTestId('b').textContent).toBe('42');
|
|
120
|
+
expect(renderCounts).toEqual({ a: 2, b: 2 });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should only trigger one re-render when signal changes inside a batch', () => {
|
|
124
|
+
const name = signal('John');
|
|
125
|
+
const surname = signal('Doe');
|
|
126
|
+
const renderSpy = vi.fn();
|
|
127
|
+
|
|
128
|
+
const TestComponent = () => {
|
|
129
|
+
renderSpy();
|
|
130
|
+
const n = useSignalValue(name);
|
|
131
|
+
const s = useSignalValue(surname);
|
|
132
|
+
return <div data-testid="full">{n} {s}</div>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
render(<TestComponent />);
|
|
136
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
137
|
+
|
|
53
138
|
act(() => {
|
|
54
|
-
|
|
139
|
+
batch(() => {
|
|
140
|
+
name.value = 'Jane';
|
|
141
|
+
surname.value = 'Smith';
|
|
142
|
+
});
|
|
55
143
|
});
|
|
56
144
|
|
|
57
|
-
expect(
|
|
145
|
+
expect(screen.getByTestId('full').textContent).toBe('Jane Smith');
|
|
146
|
+
// 2 total: 1 initial + 1 from batch (not 3)
|
|
147
|
+
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
58
148
|
});
|
|
59
149
|
});
|
|
60
150
|
|
|
61
151
|
describe('useSignal()', () => {
|
|
62
152
|
it('should maintain state across re-renders', () => {
|
|
63
153
|
let renderCount = 0;
|
|
64
|
-
|
|
154
|
+
|
|
65
155
|
const TestComponent = () => {
|
|
66
156
|
renderCount++;
|
|
67
|
-
// Create local signal
|
|
68
157
|
const localCount = useSignal(100);
|
|
69
|
-
// Subscribe to it to trigger re-renders
|
|
70
158
|
const val = useSignalValue(localCount);
|
|
71
|
-
|
|
159
|
+
|
|
72
160
|
return (
|
|
73
161
|
<div>
|
|
74
162
|
<span data-testid="val">{val}</span>
|
|
@@ -78,20 +166,52 @@ describe('@hoosk/minisignals-react adapter', () => {
|
|
|
78
166
|
};
|
|
79
167
|
|
|
80
168
|
render(<TestComponent />);
|
|
81
|
-
|
|
169
|
+
|
|
82
170
|
const valSpan = screen.getByTestId('val');
|
|
83
171
|
const btn = screen.getByText('Add');
|
|
84
172
|
|
|
85
173
|
expect(valSpan.textContent).toBe('100');
|
|
86
174
|
expect(renderCount).toBe(1);
|
|
87
175
|
|
|
88
|
-
act(() => {
|
|
89
|
-
btn.click();
|
|
90
|
-
});
|
|
176
|
+
act(() => { btn.click(); });
|
|
91
177
|
|
|
92
|
-
// The hook maintained the signal instance, and useSignalValue triggered the re-render
|
|
93
178
|
expect(valSpan.textContent).toBe('101');
|
|
94
179
|
expect(renderCount).toBe(2);
|
|
95
180
|
});
|
|
181
|
+
|
|
182
|
+
it('should return the same signal instance across re-renders', () => {
|
|
183
|
+
let capturedSignal: ReturnType<typeof useSignal<number>> | null = null;
|
|
184
|
+
let prevSignal: typeof capturedSignal = null;
|
|
185
|
+
let instanceChanged = false;
|
|
186
|
+
|
|
187
|
+
const TestComponent = () => {
|
|
188
|
+
const sig = useSignal(0);
|
|
189
|
+
if (capturedSignal !== null && capturedSignal !== sig) instanceChanged = true;
|
|
190
|
+
capturedSignal = sig;
|
|
191
|
+
const val = useSignalValue(sig);
|
|
192
|
+
return <div data-testid="v">{val}</div>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const { rerender } = render(<TestComponent />);
|
|
196
|
+
rerender(<TestComponent />);
|
|
197
|
+
rerender(<TestComponent />);
|
|
198
|
+
|
|
199
|
+
expect(instanceChanged).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should ignore the initialValue on subsequent renders', () => {
|
|
203
|
+
const TestComponent = ({ init }: { init: number }) => {
|
|
204
|
+
const sig = useSignal(init);
|
|
205
|
+
const val = useSignalValue(sig);
|
|
206
|
+
return <div data-testid="v">{val}</div>;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const { rerender } = render(<TestComponent init={1} />);
|
|
210
|
+
expect(screen.getByTestId('v').textContent).toBe('1');
|
|
211
|
+
|
|
212
|
+
// Passing a different initial value on re-render must have no effect
|
|
213
|
+
rerender(<TestComponent init={999} />);
|
|
214
|
+
expect(screen.getByTestId('v').textContent).toBe('1');
|
|
215
|
+
});
|
|
96
216
|
});
|
|
97
217
|
});
|