Scope
Review of the vue-querybuilder codebase against the goal of an idiomatic Vue 3 library that reuses @react-querybuilder/core for all query logic and preserves DOM parity with React Query Builder.
Narrowed 2026-09-21. Five of the eight original sections were already satisfied; two are refuted. See the audit comment for the full disposition table and the reasoning behind each closure. Only the open items remain below.
Architecture (context)
- Query state is owned by
QueryManager from core.
- Reactive layer:
shallowRef of immutable queries, useQueryBuilder / useRule / useRuleGroup / useQueryActions / useValueEditorReset, provide/inject for config inheritance, scoped slots for every control key.
- Four driving modes:
defaultQuery, query + onQueryChange, v-model:query, external manager.
- Conformance suite asserts byte-identical DOM output against upstream fixtures.
- DnD, UI-framework packages, expr/datetime UI, and async option lists are explicitly out of scope for v1.
No change to the overall architecture is required; the items below are refinements toward a clearer, more idiomatic Vue API surface.
Open
1. Typing helpers for authors of replacement controls
ControlComponent stays unparameterized — the reasoning at src/types/controls.ts:190-210 holds and is not reopened. The ergonomics gap it leaves is real, though, and ControlPropsMap already holds the data.
Add a public alias:
export type ControlProps<
K extends ControlKey,
F extends FullField = FullField,
O extends string = string,
> = ControlPropsMap<F, O>[K];
K constrained to core's ControlKey, so the port cannot drift from core's canonical key list.
- Exported publicly from
src/types/controls.ts.
docs/customization.md gains defineProps<Pick<ControlProps<'valueEditor'>, 'value' | 'handleOnChange'>>().
- Gate: a
src/types/types.test-d.ts case asserting mutual assignability with ValueEditorProps. Proven red by removing a member from the target.
Type-only. No runtime code, no coverage impact.
2. Pinia / store recipe (docs only)
Extend docs/customization.md "Driving the query from outside" with a Pinia subsection holding the QueryManager in a store.
Worth demonstrating rather than leaving implied: this is exactly the case core 8.23.0 made work. QueryManager now keeps its state in a non-enumerable, symbol-keyed own property carrying __v_skip, so a manager held in a store is readable through the proxy and is not deep-proxied. On an older core the same code threw Cannot read private member #past.
Scope: a fenced example only — no Pinia dependency, no examples/demo integration. Cross-reference the proxy-safety note in docs/differences-from-react-querybuilder.md §3.
3. The as unknown as slot merge, timeboxed
src/components/QueryBuilder.vue:94 casts the merged props/slots object through unknown. The likely cause is generic variance between the component's own RG parameter and the RuleGroupTypeAny that useQueryBuilder is called with — not a slot-merge typing gap, in which case a typed merge helper removes nothing.
Timebox to one sitting:
- Try a typed merge helper and see whether the cast collapses to a single
as, or disappears.
- If the variance survives (expected), keep the cast and replace it with a one-line comment naming
RG vs RuleGroupTypeAny as the reason.
Either outcome beats an unexplained double cast. Do not spend a second sitting on it.
Accepted from a refuted item
src/composables/useQueryBuilder.ts will not be split (see the audit comment), but it gains banner comments delimiting the five phases the original review named: manager lifecycle, reconfigure watcher and equality gate, resolver derivation, schema assembly, controlled write-back. Zero behavior change.
Scope
Review of the
vue-querybuildercodebase against the goal of an idiomatic Vue 3 library that reuses@react-querybuilder/corefor all query logic and preserves DOM parity with React Query Builder.Narrowed 2026-09-21. Five of the eight original sections were already satisfied; two are refuted. See the audit comment for the full disposition table and the reasoning behind each closure. Only the open items remain below.
Architecture (context)
QueryManagerfrom core.shallowRefof immutable queries,useQueryBuilder/useRule/useRuleGroup/useQueryActions/useValueEditorReset, provide/inject for config inheritance, scoped slots for every control key.defaultQuery,query+onQueryChange,v-model:query, externalmanager.No change to the overall architecture is required; the items below are refinements toward a clearer, more idiomatic Vue API surface.
Open
1. Typing helpers for authors of replacement controls
ControlComponentstays unparameterized — the reasoning atsrc/types/controls.ts:190-210holds and is not reopened. The ergonomics gap it leaves is real, though, andControlPropsMapalready holds the data.Add a public alias:
Kconstrained to core'sControlKey, so the port cannot drift from core's canonical key list.src/types/controls.ts.docs/customization.mdgainsdefineProps<Pick<ControlProps<'valueEditor'>, 'value' | 'handleOnChange'>>().src/types/types.test-d.tscase asserting mutual assignability withValueEditorProps. Proven red by removing a member from the target.Type-only. No runtime code, no coverage impact.
2. Pinia / store recipe (docs only)
Extend
docs/customization.md"Driving the query from outside" with a Pinia subsection holding theQueryManagerin a store.Worth demonstrating rather than leaving implied: this is exactly the case core 8.23.0 made work.
QueryManagernow keeps its state in a non-enumerable, symbol-keyed own property carrying__v_skip, so a manager held in a store is readable through the proxy and is not deep-proxied. On an older core the same code threwCannot read private member #past.Scope: a fenced example only — no Pinia dependency, no
examples/demointegration. Cross-reference the proxy-safety note indocs/differences-from-react-querybuilder.md§3.3. The
as unknown asslot merge, timeboxedsrc/components/QueryBuilder.vue:94casts the merged props/slots object throughunknown. The likely cause is generic variance between the component's ownRGparameter and theRuleGroupTypeAnythatuseQueryBuilderis called with — not a slot-merge typing gap, in which case a typed merge helper removes nothing.Timebox to one sitting:
as, or disappears.RGvsRuleGroupTypeAnyas the reason.Either outcome beats an unexplained double cast. Do not spend a second sitting on it.
Accepted from a refuted item
src/composables/useQueryBuilder.tswill not be split (see the audit comment), but it gains banner comments delimiting the five phases the original review named: manager lifecycle, reconfigure watcher and equality gate, resolver derivation, schema assembly, controlled write-back. Zero behavior change.