Why Signals-Based UI Libraries Work Better With AI Code Generation Than React
Source: lobsters
The frontend ecosystem spent most of the last decade optimizing for the developer writing the code. React’s component model, hooks, and one-way data flow were designed to make large, collaborative codebases manageable for human teams. Those design decisions encoded implicit assumptions about who would be authoring the code.
dlants’s article about Vamp challenges those assumptions from an unexpected direction. Vamp is a small signals-based UI library he built for Neovim plugin development, and the central claim is not primarily about performance or bundle size, though both improve significantly. Performance and bundle size improvements come with the territory when you drop a framework runtime, and dlants covers those angles. His more interesting observation is that AI-generated UI code is substantially more reliable when targeting a signals-based vanilla TypeScript model than when targeting React with its hooks and reconciliation rules.
This deserves attention beyond the Neovim context, because the argument generalizes.
What Signals Actually Do
Solid.js brought fine-grained reactivity into the mainstream JavaScript conversation, though the concept is older. Knockout.js from 2010 had observables that worked on similar principles, and Vue 3’s reactivity system, released in 2020, is built on reactive references that behave like signals under the hood.
The core mechanism: state lives in reactive primitives called signals. When you read a signal inside a reactive context (an effect, a computed value, a bound DOM attribute), the runtime records that subscription automatically. When the signal changes, only those specific dependents update. Without a virtual DOM, there is no diffing, no component re-render, no tree comparison against a previous snapshot.
A minimal example of how this looks in practice:
const count = signal(0);
const doubled = computed(() => count.value * 2);
const el = h('div', {},
h('span', { textContent: count }),
h('span', { textContent: doubled }),
h('button', { onclick: () => count.value++ }, '+1')
);
When count changes, the first <span> text updates and doubled recomputes, which updates the second <span>. Nothing else runs. The update surface is proportional to the number of DOM nodes subscribed to the changed signal, not to the size of the component tree.
The equivalent in React re-renders the component function, diffs the returned virtual tree against the previous render output, and patches the real DOM where differences are found. For a trivial example the overhead is negligible. For a Neovim plugin showing a diagnostics list with several hundred entries where a single item changes per keystroke, signals update one DOM node while React diffs hundreds of virtual nodes before concluding that one real node needs updating. Memoization (React.memo, useCallback, useMemo) can narrow that gap, but it requires correctly annotating every component and callback, which introduces exactly the kind of judgment call that AI code generators handle inconsistently.
The Benchmark Evidence
The performance gap is well documented. Stefan Krause’s JS Framework Benchmark measures DOM manipulation across frameworks on a standardized set of operations. Solid.js, which uses signals and compiles components to direct DOM operations rather than a virtual DOM, consistently outperforms React by 30 to 60 percent on row update and partial update operations. Preact Signals, introduced in 2022, bring fine-grained reactivity into Preact’s ecosystem as an escape hatch from virtual DOM diffing for hot paths. Vue 3’s ref() and reactive() primitives function as signals, which contributes to Vue 3’s significantly better benchmark numbers compared to Vue 2’s Object.defineProperty-based system.
For the Neovim plugin use case, these numbers translate directly to visible responsiveness. The plugin communicates state changes to the webview over an RPC channel. Each keystroke may trigger multiple state updates. A slow render blocks the visible update of the UI. Signals keep the update path as short as possible.
Where AI Code Generation Breaks Down
The performance argument for signals over virtual DOM has been available for years. What dlants adds is a second axis: AI code generation quality.
React has implicit contracts that appear nowhere in the code itself. The rules of hooks (never call them inside conditionals, always call the same hooks in the same order across renders) are conventions the framework enforces at runtime through internal tracking, not constraints the type system can verify. The useEffect dependency array requires the developer to enumerate all reactive dependencies manually; miss one and the effect captures stale values from a previous render, producing bugs that only appear under specific sequences of state changes.
An AI model generating a data-fetching effect must correctly identify which values in scope are stable across renders (dispatch from useReducer, refs, module-level constants) versus which are unstable (inline functions, object literals, props). The model can pattern-match on common training examples but applies those patterns inconsistently when the surrounding code deviates from the canonical form. The result is code that looks correct, compiles without errors, and behaves incorrectly in edge cases.
Signals have no equivalent ambiguity. Dependencies are tracked automatically at runtime by recording which signals are read during execution of a reactive context. There is no dependency array to maintain. The rule is: read a signal inside a computed or effect, and you have registered a dependency. The code is explicit about what depends on what, in a way that maps directly to what the runtime does. An AI generating code in this model cannot produce a stale closure bug through misapplication of a dependency array convention, because that mechanism does not exist.
This is a specific instance of a broader principle in API design: abstractions that manage implicit state for developers create conventions that must be correctly followed without being visible in the code. Those conventions are exactly what AI code generators reproduce unreliably when the surrounding context differs from their training distribution.
Why This Extends Beyond Neovim Plugins
Neovim plugin UIs have specific characteristics that sharpen the case: they are offline by nature, so server-driven HTML approaches like htmx do not apply. The DOM surface is constrained (hundreds of nodes, not tens of thousands). The audience is developers who notice startup latency and bundle size. AI-assisted development is a genuine workflow in this community.
But the argument applies broadly to any sufficiently constrained UI context: browser extensions, sidebar panels, embedded dashboards, internal tooling, admin interfaces. These are contexts where React’s 45KB minimum runtime (minified and gzipped, for React plus ReactDOM combined) is difficult to justify, and where the bundler configuration that React projects require adds surface area for AI-generated configuration errors.
A signals-based TypeScript project can build with tsc directly, producing standard ES modules without a JSX transform, without parser plugins, without loader configuration. The build toolchain becomes a non-issue rather than a source of subtle configuration bugs.
The Ecosystem Is Moving This Direction
The broader JavaScript ecosystem has been converging on reactive primitives for several years. Angular introduced signal primitives in Angular 17, shifting away from Zone.js-based change detection toward the same fine-grained model that Solid.js pioneered. Vue’s core team built reactive references as the foundation of the Composition API specifically because Object.defineProperty tracking had performance and expressiveness limits.
React’s response is different in kind: the React Compiler, previously known as React Forget, automatically inserts memoization annotations rather than adopting signals. The approach keeps the virtual DOM model intact and reduces the annotation burden without requiring developers to think in terms of reactive primitives. Whether that tradeoff holds as AI becomes a larger portion of the code authoring load is not settled. The compiler produces correct memoization for well-structured React code. It is less clear how it interacts with AI-generated code that may already include inconsistent or redundant memoization.
Picking the Right Tool for the Context
The traditional argument for React in any new project points to network effects: extensive documentation, a large training corpus in AI models, senior engineers with deep expertise, a rich ecosystem of libraries. The AI-primary development era does not erase those arguments, but it complicates them.
AI models have seen enormous amounts of React code, which is why they generate plausible-looking React. But plausible-looking and correct are not the same thing when the code involves hooks dependency arrays, memoization annotations, and reconciliation edge cases. Models trained on React code also absorb the mistakes that React code commonly contains.
Signals-based code, with its explicit dependency tracking and direct mapping between code structure and runtime behavior, gives AI generators a simpler target. The code you write is closer to what the runtime does, and that transparency benefits both the human reading the output and the model generating it.
For embedded, constrained UI contexts, the argument is fairly clear. For large-scale applications with complex routing, server integration, and organizational code ownership requirements, the ecosystem weight of React still matters. The distinction worth making is that the tool that manages complexity well for a large human team may not be the tool that produces reliable output from an AI assistant on a smaller, tighter problem. Vamp is one developer’s answer to that question for Neovim plugin UIs, but the question applies wherever the boundary conditions are similar.