Skip to content

Commit 8802208

Browse files
committed
Add running
1 parent 7a62c6d commit 8802208

File tree

4 files changed

+258
-1
lines changed

4 files changed

+258
-1
lines changed

packages/debug/README.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# @preact/signals-debug
2+
3+
A powerful debugging toolkit for [@preact/signals](https://github.com/preactjs/signals) that provides detailed insights into signal updates, effects, and computed values.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @preact/signals-debug
9+
# or
10+
yarn add @preact/signals-debug
11+
# or
12+
pnpm add @preact/signals-debug
13+
```
14+
15+
## Features
16+
17+
- Track signal value changes and updates
18+
- Monitor effect executions
19+
- Debug computed value recalculations
20+
- Get real-time debugging statistics
21+
- Configurable debugging options
22+
23+
## Usage
24+
25+
```typescript
26+
import { setDebugOptions, getDebugStats } from "@preact/signals-debug";
27+
28+
// Configure debug options
29+
setDebugOptions({
30+
grouped: true, // Group related updates in console output
31+
enabled: true, // Enable/disable debugging
32+
spacing: 2, // Number of spaces for nested update indentation
33+
});
34+
35+
// Get current debug statistics
36+
const stats = getDebugStats();
37+
console.log(stats);
38+
// Output: { activeTrackers: number, activeSubscriptions: number }
39+
```
40+
41+
## Debug Information
42+
43+
The package automatically enhances signals with debugging capabilities:
44+
45+
1. **Value Changes**: Tracks and logs all signal value changes
46+
2. **Effect Tracking**: Monitors effect executions and their dependencies
47+
3. **Computed Values**: Tracks computed value recalculations and dependencies
48+
4. **Update Grouping**: Groups related updates for better visualization
49+
5. **Performance Stats**: Provides active trackers and subscriptions count
50+
51+
## API Reference
52+
53+
### `setDebugOptions(options)`
54+
55+
Configure debugging behavior:
56+
57+
```typescript
58+
setDebugOptions({
59+
grouped?: boolean; // Enable/disable update grouping in console
60+
enabled?: boolean; // Enable/disable debugging entirely
61+
spacing?: number; // Number of spaces for nested update indentation
62+
});
63+
```
64+
65+
### `getDebugStats()`
66+
67+
Get current debugging statistics:
68+
69+
```typescript
70+
const stats = getDebugStats();
71+
// Returns:
72+
// {
73+
// activeTrackers: number, // Number of active signal trackers
74+
// activeSubscriptions: number // Number of active signal subscriptions
75+
// }
76+
```
77+
78+
### `useAsyncComputed<T>(compute: () => Promise<T> | T, options?: AsyncComputedOptions)`
79+
80+
A React hook that creates a signal that computes its value asynchronously. This is particularly useful for handling async data fetching and other asynchronous operations in a reactive way.
81+
82+
#### Type Parameters
83+
84+
- `T`: The type of the computed value
85+
86+
#### Parameters
87+
88+
- `compute`: A function that returns either a Promise or a direct value
89+
- `options`: Configuration options
90+
- `suspend?: boolean`: Whether to enable Suspense support (defaults to true)
91+
92+
#### Returns
93+
94+
An `AsyncComputed<T>` object with the following properties:
95+
96+
- `value: T | undefined`: The current value (undefined while loading)
97+
- `error: Signal<unknown>`: Signal containing any error that occurred
98+
- `running: Signal<boolean>`: Signal indicating if the computation is in progress
99+
100+
#### Example
101+
102+
```typescript
103+
import { useAsyncComputed } from "@preact/signals/utils";
104+
105+
function UserProfile({ userId }: { userId: Signal<string> }) {
106+
const userData = useAsyncComputed(
107+
async () => {
108+
const response = await fetch(`/api/users/${userId.value}`);
109+
return response.json();
110+
},
111+
{ suspend: false }
112+
);
113+
114+
if (userData.running.value) {
115+
return <div>Loading...</div>;
116+
}
117+
118+
if (userData.error.value) {
119+
return <div>Error: {String(userData.error.value)}</div>;
120+
}
121+
122+
return (
123+
<div>
124+
<h1>{userData.value?.name}</h1>
125+
<p>{userData.value?.email}</p>
126+
</div>
127+
);
128+
}
129+
```
130+
131+
The hook will automatically:
132+
133+
- Recompute when dependencies change (e.g., when `userId` changes)
134+
- Handle loading and error states
135+
- Clean up subscriptions when the component unmounts
136+
- Cache results between re-renders
137+
- Support React Suspense when `suspend: true`
138+
139+
## License
140+
141+
MIT © [Preact Team](https://preactjs.com)

packages/preact/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,68 @@ function Component() {
189189
}
190190
```
191191
192+
### `useAsyncComputed<T>(compute: () => Promise<T> | T, options?: AsyncComputedOptions)`
193+
194+
A Preact hook that creates a signal that computes its value asynchronously. This is particularly useful for handling async data fetching and other asynchronous operations in a reactive way.
195+
196+
> You can also import `asyncComputed` as a non-hook way
197+
198+
#### Parameters
199+
200+
- `compute`: A function that returns either a Promise or a direct value.
201+
Using signals here will track them, when the signal changes it will re-execute `compute`.
202+
- `options`: Configuration options
203+
- `suspend?: boolean`: Whether to enable Suspense support (defaults to true)
204+
205+
#### Returns
206+
207+
An `AsyncComputed<T>` object with the following properties:
208+
209+
- `value: T | undefined`: The current value (undefined while loading)
210+
- `error: Signal<unknown>`: Signal containing any error that occurred
211+
- `running: Signal<boolean>`: Signal indicating if the computation is in progress
212+
213+
> When inputs to `compute` change the value and error will be retained but `running` will be `true`.
214+
215+
#### Example
216+
217+
```typescript
218+
import { useAsyncComputed } from "@preact/signals/utils";
219+
220+
function UserProfile({ userId }: { userId: Signal<string> }) {
221+
const userData = useAsyncComputed(
222+
async () => {
223+
const response = await fetch(`/api/users/${userId.value}`);
224+
return response.json();
225+
},
226+
{ suspend: false }
227+
);
228+
229+
if (userData.running.value) {
230+
return <div>Loading...</div>;
231+
}
232+
233+
if (userData.error.value) {
234+
return <div>Error: {String(userData.error.value)}</div>;
235+
}
236+
237+
return (
238+
<div>
239+
<h1>{userData.value?.name}</h1>
240+
<p>{userData.value?.email}</p>
241+
</div>
242+
);
243+
}
244+
```
245+
246+
The hook will automatically:
247+
248+
- Recompute when dependencies change (e.g., when `userId` changes)
249+
- Handle loading and error states
250+
- Clean up subscriptions when the component unmounts
251+
- Cache results between re-renders
252+
- Support Suspense when `suspend: true`
253+
192254
## License
193255
194256
`MIT`, see the [LICENSE](../../LICENSE) file.

packages/preact/utils/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ interface AugmentedPromise<T> extends Promise<T> {
8484
interface AsyncComputed<T> extends Signal<T> {
8585
value: T;
8686
error: Signal<unknown>;
87+
running: Signal<boolean>;
8788
pending?: AugmentedPromise<T> | null;
8889
/** @internal */
8990
_cleanup(): void;
@@ -108,8 +109,13 @@ export function asyncComputed<T>(
108109
): AsyncComputed<T | undefined> {
109110
const out = signal<T | undefined>(undefined) as AsyncComputed<T | undefined>;
110111
out.error = signal<unknown>(undefined);
112+
out.running = signal<boolean>(false);
111113

112114
const applyResult = (value: T | undefined, error?: unknown) => {
115+
if (out.running.value) {
116+
out.running.value = false;
117+
}
118+
113119
if (out.pending) {
114120
out.pending.error = error;
115121
out.pending.value = value;
@@ -142,10 +148,14 @@ export function asyncComputed<T>(
142148
return applyResult(result.value as T);
143149
}
144150

151+
out.running.value = true;
152+
145153
// Handle async resolution
146154
out.pending = result.then(
147155
(value: T) => {
148-
applyResult(value);
156+
if (currentId === computeCounter) {
157+
applyResult(value);
158+
}
149159
return value;
150160
},
151161
(error: unknown) => {
@@ -156,6 +166,7 @@ export function asyncComputed<T>(
156166
}
157167
) as AugmentedPromise<T>;
158168
} else {
169+
out.running.value = false;
159170
applyResult(result);
160171
}
161172
} catch (error) {
@@ -188,6 +199,7 @@ export function useAsyncComputed<T>(
188199
const incoming = asyncComputed(() => computeRef.current());
189200

190201
if (cached) {
202+
incoming.running = cached.running;
191203
incoming.value = cached.value;
192204
incoming.error.value = cached.error.peek();
193205
cached._cleanup();

packages/preact/utils/test/browser/index.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,47 @@ describe("@preact/signals-utils", () => {
160160
});
161161
expect(scratch.innerHTML).to.eq("<p>baz</p>");
162162
});
163+
164+
it("Should apply the 'running' signal", async () => {
165+
const AsyncComponent = (props: any) => {
166+
const data = useAsyncComputed<{ foo: string }>(
167+
async () => fetchResult(props.url.value),
168+
{ suspend: false }
169+
);
170+
const hasData = data.value !== undefined;
171+
return (
172+
<p>
173+
{data.running.value
174+
? "running"
175+
: hasData
176+
? data.value?.foo
177+
: "error"}
178+
</p>
179+
);
180+
};
181+
const url = signal("/api/foo?id=1");
182+
act(() => {
183+
render(<AsyncComponent url={url} />, scratch);
184+
});
185+
expect(scratch.innerHTML).to.eq("<p>running</p>");
186+
187+
await act(async () => {
188+
await resolve({ foo: "bar" });
189+
await new Promise(resolve => setTimeout(resolve));
190+
});
191+
192+
expect(scratch.innerHTML).to.eq("<p>bar</p>");
193+
194+
act(() => {
195+
url.value = "/api/foo?id=2";
196+
});
197+
expect(scratch.innerHTML).to.eq("<p>running</p>");
198+
199+
await act(async () => {
200+
await resolve({ foo: "baz" });
201+
await new Promise(resolve => setTimeout(resolve));
202+
});
203+
expect(scratch.innerHTML).to.eq("<p>baz</p>");
204+
});
163205
});
164206
});

0 commit comments

Comments
 (0)