feat(scan-react): parallel idiomatic-React port of the Preact toolbar UI#458
feat(scan-react): parallel idiomatic-React port of the Preact toolbar UI#458aidenybai wants to merge 1 commit into
Conversation
Adds packages/scan-react, a parallel React port of the react-scan toolbar UI that currently lives in packages/scan/src/web (Preact). The original Preact package is left untouched. - All 37 Preact UI files ported to React; 0 preact imports remain. - Signals replaced with a React-idiomatic compat layer (src/web/utils/signals.ts) over useSyncExternalStore: module-level signal()/computed() keep their .value/.subscribe store API; render-body reads become useSignalValue/useComputed hooks; trivial useSignal -> useState. - render() -> createRoot()/root.unmount(); preact/compat + preact/hooks -> react; constant() HOC -> memo(C, () => true); class error boundary ported. - JSX idioms normalized (class->className, onInput->onChange, SVG camelCase, React event types). - The framework-agnostic profiling engine is re-used as-is from react-scan via the ~core/* alias (no core edits); cross-package resolution handled purely in tsconfig + a local global.d.ts. - tsc --noEmit passes with 0 errors. See packages/scan-react/PORT_SPEC.md for the full porting conventions. This is exploratory and ships nothing in production.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit da5dde3. Configure here.
| if (this.dirty) this.recompute(); | ||
| return this.current; | ||
| } | ||
| } |
There was a problem hiding this comment.
Computed never marks dirty before dependency-triggered recompute
Medium Severity
When a dependency signal changes, the Computed's reaction calls recompute() directly without first setting this.dirty = true. If any code reads the computed via peek() or .value during the notification cascade (before this particular computed's reaction runs), it will return the stale cached value because dirty is still false and read() short-circuits. The reaction constructor stores () => this.recompute() but the computed should be marked dirty so any intermediate reads see the need for recalculation.
Reviewed by Cursor Bugbot for commit da5dde3. Configure here.
| const ref = useRef<Computed<T> | null>(null); | ||
| if (ref.current === null) ref.current = computed(compute); | ||
| return useSignalValue(ref.current); | ||
| } |
There was a problem hiding this comment.
useComputed captures stale compute closure forever
Medium Severity
useComputed stores the compute function in a ref that is only set once (on mount). If the compute closure captures component props or local state, those values become permanently stale. The Computed instance will always re-run the original closure, ignoring any prop/state updates that don't come through signals.
Reviewed by Cursor Bugbot for commit da5dde3. Configure here.
| const timeout = setTimeout(() => setDelayedValue(value), delay); | ||
|
|
||
| return () => clearTimeout(timeout); | ||
| }, [value, onDelay, offDelay]); |
There was a problem hiding this comment.
useDelayedValue stale closure skips necessary timeout
Low Severity
The useEffect reads delayedValue but omits it from the dependency array. When value toggles rapidly (e.g., true→false→true before the first timeout fires), the effect re-runs with the new value but the stale delayedValue from the closure. The value === delayedValue early-return check can incorrectly match against the stale value, causing the hook to skip scheduling the timeout and leaving delayedValue stuck at its previous state.
Reviewed by Cursor Bugbot for commit da5dde3. Configure here.


Summary
Adds
packages/scan-react, a parallel idiomatic-React port of the react-scan toolbar UI that currently lives inpackages/scan/src/web(Preact). The existing Preact package is left completely untouched — this is additive and exploratory, and ships nothing in production.preact/@preactimports remain.tsc --noEmitpasses with 0 errors.How it works
Signals → idiomatic React
A hand-written compat layer (
src/web/utils/signals.ts) replaces@preact/signals:signal()/computed()keep their.value/.peek()/.subscribe()store API, so non-render code and the shared engine work unchanged.useSignalValue/useComputedhooks (backed byuseSyncExternalStore).useSignal→useState;useSignalEffectretained via the compat module.Framework API + JSX
render(vnode, container)→createRoot(container).render(...)/root.unmount()(the old double-render(null)unmount hack is gone).preact/hooks+preact/compat→react(createPortalfromreact-dom).constant()HOC: Preact'sshouldComponentUpdate = false→memo(Component, () => true).React.Component.class→className,onInput→onChange,onDblClick→onDoubleClick, SVG kebab→camelCase, React synthetic event types.Engine re-use (no core edits)
The framework-agnostic profiling engine is re-used as-is from
react-scanvia the~core/*alias — no core files were modified. Cross-package resolution is handled purely intsconfig.json(asrc/*path mapping + including upstreamtypes.ts) and a localglobal.d.ts(*.cssambient module).react/bippyare pinned to dedupe with the original package.See
packages/scan-react/PORT_SPEC.mdfor the full porting conventions.react-scan deliberately uses Preact for its own toolbar to avoid (a) instrumenting its own UI, (b) adding a second reconciler / coupling to the host app's React version, and (c) bundle bloat. A React port re-introduces all three — hence this lives as an isolated parallel package rather than replacing the Preact UI.
Not included (follow-ups)
auto.ts-equivalent mount entry — typecheck-validated but not bundled.Test plan
pnpm --filter react-scan-react typecheck→ 0 errorspreact/@preactimports remain inpackages/scan-react/srcpackages/scan(Preact original) unchanged🤖 Generated with Claude Code
Note
Medium Risk
Large new UI surface and a second React reconciler in the host if ever mounted, but isolated private package with no core edits and no production bundle wiring yet.
Overview
Introduces
packages/scan-react, a private parallel package that copies the react-scan toolbar UI from Preact into idiomatic React whilepackages/scanstays unchanged.The port adds a
~web/utils/signalslayer so module-levelsignal/computedkeep.value/.subscribefor handlers and shared~core, while components subscribe viauseSignalValue/useComputed(useSyncExternalStore). Toolbar mounting moves tocreateRoot/root.unmount()with a ReactComponenterror boundary; JSX and types are normalized for React (className,onChange, synthetic events, etc.).PORT_SPEC.mddocuments the full Preact→React mapping. TypeScript wires~core/*andsrc/*to../scan/srcso the profiling engine is reused without editing core;global.d.tsadds*.cssmodules. The diff includes the full Tailwind/CSS asset tree plus shared UI pieces (widget shell, inspector tree, notifications, hooks, utilities).package.jsonis typecheck-only (no tsup / auto mount yet)—exploratory, not wired into production.Reviewed by Cursor Bugbot for commit da5dde3. Bugbot is set up for automated code reviews on this repo. Configure here.