Skip to content

Commit 7e2069d

Browse files
authored
Merge pull request #851 from preactjs/special-component-update
Add special case for component renders
2 parents 01a0dd6 + f5a6935 commit 7e2069d

File tree

13 files changed

+140
-50
lines changed

13 files changed

+140
-50
lines changed

.changeset/dull-walls-bathe.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@preact/signals-devtools-adapter": minor
3+
"@preact/signals-devtools-ui": minor
4+
"@preact/signals-debug": minor
5+
---
6+
7+
Add special case in devtools-ui for component updates

.changeset/poor-snails-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals-debug": minor
3+
---
4+
5+
Add special copy for component renders

packages/debug/src/devtools.ts

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88

99
/** Formatted signal update for external consumers */
1010
export interface FormattedSignalUpdate {
11-
type: "update" | "effect";
12-
signalType: "signal" | "computed" | "effect";
11+
type: "update" | "effect" | "component";
12+
signalType: "signal" | "computed" | "effect" | "component";
1313
signalName: string;
1414
signalId: string;
1515
prevValue?: any;
@@ -24,7 +24,7 @@ export interface FormattedSignalUpdate {
2424
/** Formatted signal disposal event for external consumers */
2525
export interface FormattedSignalDisposed {
2626
type: "disposed";
27-
signalType: "signal" | "computed" | "effect";
27+
signalType: "signal" | "computed" | "effect" | "component";
2828
signalName: string;
2929
signalId: string;
3030
timestamp: number;
@@ -144,25 +144,35 @@ class DevToolsCommunicator {
144144
}
145145

146146
const formattedUpdates = updateInfoList.map(({ signal, ...info }) => {
147-
return info.type === "value"
148-
? {
149-
...info,
150-
type: "update" as const,
151-
newValue: deeplyRemoveFunctions(info.newValue),
152-
prevValue: deeplyRemoveFunctions(info.prevValue),
153-
signalType: ("_fn" in signal ? "computed" : "signal") as
154-
| "signal"
155-
| "computed",
156-
signalName: this.getSignalName(signal, false),
157-
signalId: this.getSignalId(signal),
158-
}
159-
: {
160-
...info,
161-
type: "effect" as const,
162-
signalType: "effect" as const,
163-
signalName: this.getSignalName(signal, true),
164-
signalId: this.getSignalId(signal),
165-
};
147+
if (info.type === "value") {
148+
return {
149+
...info,
150+
type: "update" as const,
151+
newValue: deeplyRemoveFunctions(info.newValue),
152+
prevValue: deeplyRemoveFunctions(info.prevValue),
153+
signalType: ("_fn" in signal ? "computed" : "signal") as
154+
| "signal"
155+
| "computed",
156+
signalName: this.getSignalName(signal, "value"),
157+
signalId: this.getSignalId(signal),
158+
};
159+
} else if (info.type === "component") {
160+
return {
161+
...info,
162+
type: "component" as const,
163+
signalType: "component" as const,
164+
signalName: this.getSignalName(signal, "component"),
165+
signalId: this.getSignalId(signal),
166+
};
167+
} else {
168+
return {
169+
...info,
170+
type: "effect" as const,
171+
signalType: "effect" as const,
172+
signalName: this.getSignalName(signal, "effect"),
173+
signalId: this.getSignalId(signal),
174+
};
175+
}
166176
});
167177

168178
// Emit for direct listeners (e.g., DirectAdapter)
@@ -212,8 +222,8 @@ class DevToolsCommunicator {
212222
};
213223
}
214224

215-
public getSignalName(signal: any, isEffect: boolean): string {
216-
return getSignalName(signal, isEffect);
225+
public getSignalName(signal: any, type: any): string {
226+
return getSignalName(signal, type);
217227
}
218228

219229
public getSignalId(signal: any): string {
@@ -231,7 +241,10 @@ class DevToolsCommunicator {
231241
const disposal: FormattedSignalDisposed = {
232242
type: "disposed",
233243
signalType,
234-
signalName: this.getSignalName(signal, signalType === "effect"),
244+
signalName: this.getSignalName(
245+
signal,
246+
signalType === "signal" ? "value" : signalType
247+
),
235248
signalId: this.getSignalId(signal),
236249
timestamp: Date.now(),
237250
};

packages/debug/src/index.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ function getAllCurrentDependencies(
228228
if (!dependencies.has(id)) {
229229
dependencies.set(id, {
230230
id,
231-
name: getSignalName(source, false),
231+
name: getSignalName(source, 'value'),
232232
type: "_fn" in source ? "computed" : "signal",
233233
});
234234
}
@@ -285,7 +285,7 @@ Effect.prototype._debugCallback = function (this: Effect) {
285285
signal: this,
286286
timestamp: Date.now(),
287287
depth: baseSignal.depth,
288-
type: "effect",
288+
type: "component",
289289
subscribedTo: getSignalId(baseSignal.signal),
290290
allDependencies: getAllCurrentDependencies(this as any),
291291
});
@@ -299,7 +299,24 @@ Effect.prototype._callback = function (this: Effect) {
299299
if (!debugEnabled || internalEffects.has(this))
300300
return originalEffectCallback.call(this);
301301

302-
this._debugCallback!();
302+
if ("_sources" in this) {
303+
const baseSignal = bubbleUpToBaseSignal(this as any);
304+
if (baseSignal) {
305+
// Track dependency
306+
trackDependency(this, baseSignal.signal);
307+
308+
const updateInfoList = updateInfoMap.get(baseSignal.signal) || [];
309+
updateInfoList.push({
310+
signal: this,
311+
timestamp: Date.now(),
312+
depth: baseSignal.depth,
313+
type: "effect",
314+
subscribedTo: getSignalId(baseSignal.signal),
315+
allDependencies: getAllCurrentDependencies(this as any),
316+
});
317+
updateInfoMap.set(baseSignal.signal, updateInfoList);
318+
}
319+
}
303320

304321
return originalEffectCallback.call(this);
305322
};
@@ -343,10 +360,7 @@ function flushUpdates() {
343360
if (typeof window !== "undefined" && !bridge.shouldThrottleUpdate()) {
344361
// Filter updates based on signal names
345362
const filteredUpdates = updateInfoList.filter(updateInfo => {
346-
const signalName = getSignalName(
347-
updateInfo.signal,
348-
updateInfo.type === "effect"
349-
);
363+
const signalName = getSignalName(updateInfo.signal, updateInfo.type);
350364
return bridge.matchesFilter(signalName);
351365
});
352366

@@ -375,18 +389,19 @@ function logUpdate(info: UpdateInfo, prevDepth: number) {
375389
if (!debugEnabled || !consoleLoggingEnabled) return;
376390

377391
const { signal, type, depth } = info;
378-
const name = getSignalName(signal, type === "effect");
392+
const name = getSignalName(signal, type);
379393

380-
if (type === "effect") {
394+
if (type === "effect" || type === "component") {
381395
if (prevDepth === depth) {
382396
endUpdateGroup();
383397
}
384398

399+
const copy = type === "effect" ? "effect" : "component render";
385400
if (isGrouped)
386401
console.groupCollapsed(
387-
`${" ".repeat(depth * 2)}↪️ Triggered effect: ${name}`
402+
`${" ".repeat(depth * 2)}↪️ Triggered ${copy}: ${name}`
388403
);
389-
else console.log(`${" ".repeat(depth * 2)}↪️ Triggered effect: ${name}`);
404+
else console.log(`${" ".repeat(depth * 2)}↪️ Triggered ${copy}: ${name}`);
390405
return;
391406
}
392407

packages/debug/src/internal.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface DependencyInfo {
2222
type: "signal" | "computed";
2323
}
2424

25-
export type UpdateInfo = ValueUpdate | EffectUpdate;
25+
export type UpdateInfo = ValueUpdate | EffectUpdate | ComponentUpdate;
2626

2727
export interface ValueUpdate {
2828
type: "value";
@@ -43,3 +43,12 @@ interface EffectUpdate {
4343
subscribedTo?: string; // signalId of the signal this effect is subscribed to
4444
allDependencies?: DependencyInfo[]; // All dependencies this effect depends on
4545
}
46+
47+
interface ComponentUpdate {
48+
type: "component";
49+
timestamp: number;
50+
signal: Effect;
51+
depth: number;
52+
subscribedTo?: string; // signalId of the signal this component is subscribed to
53+
allDependencies?: DependencyInfo[]; // All dependencies this component depends on
54+
}

packages/debug/src/utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { Effect, Signal } from "@preact/signals-core";
22

3-
export function getSignalName(signal: any, isEffect: boolean): string {
3+
export function getSignalName(
4+
signal: any,
5+
type: "component" | "effect" | "value"
6+
): string {
47
// Try to get a meaningful name for the signal
58
if (signal.displayName) return signal.displayName;
69
if (signal.name)
710
return signal.name === "sub"
811
? `${(signal as Effect)._sources?._source.name}-subscribe`
912
: signal.name;
1013
if (signal._fn && signal._fn.name) return signal._fn.name;
11-
if (isEffect) return "effect";
14+
if (type === "effect" || type === "component") return type;
1215
const signalType = "_fn" in signal ? "computed" : "signal";
1316
return `(anonymous ${signalType})`;
1417
}

packages/debug/test/browser/utils.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,12 @@ describe("formatValue", () => {
184184
describe("getSignalName", () => {
185185
it("should return signal name if present", () => {
186186
const sig = signal(0, { name: "counter" });
187-
expect(getSignalName(sig, false)).toBe("counter");
187+
expect(getSignalName(sig, "value")).toBe("counter");
188188
});
189189

190190
it("should return (anonymous signal) for unnamed signals", () => {
191191
const sig = signal(0);
192-
expect(getSignalName(sig, false)).toBe("(anonymous signal)");
192+
expect(getSignalName(sig, "value")).toBe("(anonymous signal)");
193193
});
194194

195195
it("should return computed name if present", () => {

packages/devtools-adapter/src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export interface DependencyInfo {
1111
* Represents a signal update event from the debug system
1212
*/
1313
export interface SignalUpdate {
14-
type: "update" | "effect";
15-
signalType: "signal" | "computed" | "effect";
14+
type: "update" | "effect" | "component";
15+
signalType: "signal" | "computed" | "effect" | "component";
1616
signalName: string;
1717
signalId?: string;
1818
prevValue?: any;
@@ -30,7 +30,7 @@ export interface SignalUpdate {
3030
*/
3131
export interface SignalDisposed {
3232
type: "disposed";
33-
signalType: "signal" | "computed" | "effect";
33+
signalType: "signal" | "computed" | "effect" | "component";
3434
signalName: string;
3535
signalId: string;
3636
timestamp: number;

packages/devtools-ui/src/components/Graph.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ export function GraphVisualization() {
212212
if (!update.signalId) continue;
213213
if (!showDisposed && disposed.has(update.signalId)) continue;
214214

215-
const type: "signal" | "computed" | "effect" = update.signalType;
215+
const type: "signal" | "computed" | "effect" | "component" =
216+
update.signalType;
216217

217218
if (!nodes.has(update.signalId)) {
218219
nodes.set(update.signalId, {
@@ -424,6 +425,9 @@ export function GraphVisualization() {
424425
case "effect":
425426
lines.push(` ${id}([${name}])`);
426427
break;
428+
case "component":
429+
lines.push(` ${id}{{${name}}}`);
430+
break;
427431
}
428432
});
429433

@@ -655,6 +659,13 @@ export function GraphVisualization() {
655659
></div>
656660
<span>Effect</span>
657661
</div>
662+
<div className="legend-item">
663+
<div
664+
className="legend-color"
665+
style={{ backgroundColor: "#9c27b0" }}
666+
></div>
667+
<span>Component</span>
668+
</div>
658669
</div>
659670
</div>
660671
</div>

packages/devtools-ui/src/components/UpdateItem.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@ export function UpdateItem({ update, count, firstUpdate }: UpdateItemProps) {
3131
</span>
3232
);
3333

34-
if (update.type === "effect") {
34+
if (update.type === "effect" || update.type === "component") {
35+
const icon = update.type === "component" ? "🔄" : "↪️";
36+
const label = update.type === "component" ? "Component render" : "Effect";
3537
return (
3638
<div className={`update-item ${update.type}`}>
3739
<div className="update-header">
3840
<span className="signal-name">
39-
↪️ {update.signalName}
41+
{icon} {update.signalName}
4042
{countLabel}
4143
</span>
44+
<span className="update-type-badge">{label}</span>
4245
<span className="update-time">{time}</span>
4346
</div>
4447
</div>

0 commit comments

Comments
 (0)