Pitfalls

July 21, 2026 ยท View on GitHub

โ† Guide index

Read this before debugging. Each row is a real failure mode of the signals-first model. ๐Ÿค– Agents: scan the Symptom column to match what you're seeing.

Reactivity

SymptomCauseFix
A run-once component shows a stale value foreverA Component whose Render() reads no signals renders once; Ui.Text(sig.Value) read the value one timeUse a bound prop: new TextEl("") { Text = sig } (signal-direct) or Text = Prop.Of(() => f(sig.Value)). In a render that should stay run-once, everything dynamic must be a bind / For / Show.
A binding never updatesThe thunk used .Peek() (no subscribe) โ€” or read a plain field, not a signalRead .Value inside the thunk: Transform = Prop.Of(() => Affine2D.Translation(sig.Value, 0)). .Value subscribes the binding effect; .Peek() does not.
Setting a signal does nothing visibleWhatever should react never read that signal (no subscription exists)Make the renderer/binding read signal.Value (in Render() to re-render, or in a bind thunk to update a node).
One cell stays stale after a list refreshA bound row reads a reactive slot index but its bind captured a mount-time collection/snapshot; a fresh Prop.Of thunk on re-render does not replace the mounted thunkUse BoundItems.From/BoundItems.Project with typed ItemsView.CreateBound<T>, and read scope.Item.Value inside every bound cell. Resolve actions from the current source instead of capturing an earlier row value.
A child component ignores new data from its parentConstructor args are frozen at mount โ€” the factory isn't re-invoked on parent re-renderPass a Signal<T> or use context; the child reads sig.Value/UseContext and updates. Control factories should use a Props record + Ctx.Provide for runtime-changeable props. See reactivity.md.
Infinite re-render / Flush exceeded 1000 iterations in logsA setState/signal write happens during render (a render re-schedules itself)Move the write into an event handler or UseEffect. Never write a signal a component reads from inside its own Render().
Hooks throw / state lands in the wrong cellHooks called conditionally or in a loop โ€” slot order shiftedCall hooks unconditionally, top-level, same order every render (the React rules-of-hooks; cells are slot-indexed).
UseContext returns the default, not the provided valueNo Ctx.Provide ancestor for that channel above this componentWrap the subtree in Ctx.Provide(channel, value, child); the consumer must be a descendant.
For/Show doesn't updateThe When/Count/ItemAt thunk read .Peek() or a non-signalRead .Value inside the thunk so the boundary effect subscribes.

Performance

SymptomCauseFix
Dragging a slider tanks FPSA setState per pointer-move re-renders the owning component every framePass a FloatSignal to Slider.Create (compositor bypass โ€” the one slider API), or hand-bind the value to Transform. Confirm FrameStats.Rendered == false on drag.
A small change relayouts the whole pageNo layout boundary above the change โ†’ the up-walk reaches the root โ†’ full layoutGive the enclosing container explicit Width+Height+ClipToBounds=true so it's a boundary. See rendering-and-performance.md.
HotPhaseAllocBytes > 0 (zero-alloc check fails)Allocation inside a bind thunk or hot effect body (new, LINQ, boxing, per-call closure)Capture everything once at mount; the thunk must only read + write existing state. No allocation in phases 6โ€“13.
Whole app re-renders on one interactionState lives too high (at the root), so the root's render-effect runsMove state down into the component that owns it; or bind the hot value instead of setState.
List with thousands of items is slow / leaks nodesNot virtualized, or no stable keyOfUse Virtual.List/Repeater.ItemsRepeater with keyOf: i => stableId. Only the window is realized; rows recycle.
Scroll re-realizes/relayouts every frame(rare) something marks the viewport dirty each frameIn-window scroll is transform-only by design; check you aren't re-rendering the list component each frame (move its state out / bind it).
A virtualized list/grid allocates on every scroll frame (HotPhaseAllocBytes > 0)A per-item closure or a per-item TemplateParts modifier is rebuilt on each realize; or a PartDelta lambda calls new/Animate/LINQ per itemVary per-item chrome through PartDelta VALUES only (pure-value lambda, no allocation); keep the skin on the ContainerFactory/SelectorVisual seam; per-item structure uses Opacity=0/Width=0 invisible-part flips, never add/remove children. See control-fidelity ยง6.

Layout & visuals

SymptomCauseFix
Content vanishes / is clipped to a tiny boxA ClipToBounds ancestor has zero/!wrong size, or a presented-size animation clipped it on frame 1Check the node's Width/Height; dump with FG_DUMP=1. Boundaries need real sizes.
A static OffsetX/ScaleX/Opacity you set "snaps back" each frameAn animation or a bound Transform/Opacity owns that channel; the reconciler won't also write the static value (else it'd fight the animator)Drive the value through the bind/animation, not both. Pick one owner per channel.
Element not clickableHitTestVisible = false, zero size, or no handlerGive it size and an OnClick/OnPointerDown; HitTestVisible defaults true.
Text doesn't wrapWrap = NoWrap (default) or no width constraint to wrap againsttext.Wrapped() + a bounded width (explicit Width or a stretching parent).
Colors look wrong across themesHard-coded ColorF instead of tokensRead Tok.* (e.g. Tok.TextPrimary, Tok.FillCardDefault); they follow Tok.Use(theme).
UseMeasuredBounds/UseMeasuredWidth re-renders every frame (a "measured-bounds feedback loop")The rendered root's size is derived from its own measured size โ€” reading the measured value changes layout, which changes the measured value, foreverMeasure an outer, fixed node instead of one whose size the render controls, or add a quantum to UseMeasuredWidth to absorb sub-quantum wobble. The value lands one frame late by design (written during layout โ‡’ re-render next frame โ€” never same-frame); a same-frame layout-effect sees the previous value. FG_DIAG=1 (DEBUG) warns after >8 consecutive changing frames.

Lifecycle & effects

SymptomCauseFix
UseEffect cleanup never runsThe Action overload has no return-cleanup channelReturn an Action?: UseEffect(() => { โ€ฆ; return () => dispose(); }). The returned cleanup runs before each re-run and once at unmount (auto-tracked or deps-gated).
Cleanup-returning effect never re-runs / a fire-only effect fires only onceThe lambda's returned Action bound to the cleanup overloadIntentional for a cleanup effect. For a fire-only effect write a block body () => { X(); } (no return <expr>) so it binds to the plain Action overload.
A no-deps UseEffect(fn) re-runs unexpectedlyIt's auto-tracked now (the default): any signal the body reads re-runs itIf you want run-once, pass DepKey.Empty: UseEffect(fn, DepKey.Empty). If you want it keyed, pass a DepKey of the values it should follow.
UseEffect(fn, DepKey.FromRef(x)) re-runs every renderA fresh lambda/instance each render is a new identityFromRef keys on object identity, not Equals โ€” pass a stable instance (hold it in UseRef/UseMemo), or key on a scalar/string instead. An in-place mutation of the same instance does not re-fire.
UseEffect(fn, someArray) won't compileparams object[] deps were removed โ€” the one dep shape is DepKeyConvert: scalars/tuples convert implicitly (, count / , (name, i)); >4 scalars use DepKey.From(HashCode.Combine(...)) or DepKey.Combine; mount-once is DepKey.Empty.
Animation hook seems to do nothingThe hook seeds on HostNode, set after the first layout; or deps never changedAnimation hooks (UseSpring etc.) run in phase 6.5 once mounted; pass a DepKey deps that changes to re-target (DepKey.Empty = seed once).
A removed node lingers brieflyIt has an exit animation (BoxEl.Animate with an Exit) โ€” it's an orphan animating outExpected; it's reclaimed on settle. Not a leak.
State lost when a component's element type changes at a positionType-flip at a position remounts (state-loss is intentional)Keep the element type stable at a position, or use a Key/For to preserve identity.

Workflow (๐Ÿค– agents especially)

SymptomCauseFix
"It builds, ship it" but a seam regressedDidn't run the cross-seam harnessdotnet run --project src/FluentGpu.VerticalSlice โ†’ require ALL CHECKS PASSED before claiming done.
Canon gate fails after editing docs/design/A stale/superseded token reappeared in the live design treeFix the token, or add <!-- canon-allow: reason -->; re-run powershell -File docs/design/check-canon.ps1. Usage docs go in docs/guide/, not docs/design/ (the gate scans docs/design/ only).
Added an Element type but it doesn't renderNot wired into the reconcilerGive it a free ElementTypeId, then handle it in Reconciler.Mount/Update (and ChildrenOf if it has children).
AOT publish fails at the native link stepThe shell isn't a VS Developer environment (link.exe/vswhere not on PATH)The managed/IL-AOT analysis still validated; run the publish from a VS Developer prompt for the final native link. Don't treat the link error as a code defect.
Claimed a fix works without evidenceNo verification runShow the harness output / FrameStats (Rendered, ComponentsRendered, HotPhaseAllocBytes). Evidence before assertions.

Analyzer diagnostics (FGRP)

The in-repo Roslyn analyzer flags reactivity mistakes at build time (FluentGpu.SourceGen; warnings unless noted).

SymptomRuleFix
Frozen Element content slot โ€” a child never shows updated contentFGRP001 (Warning)Content assigned as a field inside Embed.Comp(() => new T { โ€ฆ }) freezes at mount. Re-push via Embed.Comp(props, factory) + [Props]/UseProps<T>, use Ctx.Provide, or remount with a changed Key.
A bound channel never updates after the first renderFGRP002 (Warning)A Prop.Of thunk captured a var v = sig.Value; snapshot. Read the signal's .Value inside the thunk instead.
A bind thunk fires once then goes deadFGRP003 (Warning)The Prop.Of thunk reads only .Peek() (no subscription). Read .Value inside it, or bind the signal directly.
An element expression statement has no effectFGRP004 (Warning)Element is immutable; a modifier (box.Rounded(8);) or new BoxEl { โ€ฆ }; returns a new element you discarded โ€” return/assign/add it to a children list.
A hook in a loop shifts state between iterations on reorderFGRP005 (Info)Positional-cell hooks key by per-line ordinal + index; key rows with Flow.For (a keyed child per item) or hoist the state out of the loop. Conditional hooks are fine.
A dep-gated effect/memo re-runs every renderFGRP006 (Warning)DepKey.FromRef(new โ€ฆ/lambda) keys off a fresh identity each render. Hoist the reference or key on a value projection (DepKey.From(...)).
A declared channel silently paints default(T)FGRP007 (Warning)default(Prop<T>) is a static default(T), not "unset". Give every Prop<T> { get; init; } channel on an Element record an explicit initializer (= Tok.Foo / = 1f / = default).

Quick self-check before committing an engine change

  1. dotnet build src/FluentGpu.VerticalSlice โ€” clean.
  2. dotnet run --project src/FluentGpu.VerticalSlice โ€” ALL CHECKS PASSED.
  3. If you touched reactivity/layout/render: confirm FrameStats.Rendered/ComponentsRendered/HotPhaseAllocBytes are what you expect on the relevant interaction (add a Check).
  4. If you edited docs/design/: powershell -File docs/design/check-canon.ps1 exits 0.