Part of #68 (Phase 1) — the final slice of the #88 reactivity migration. Links to roadmap #68; foundation in docs/ADR-0001-reactivity.md + CLAUDE.md rule 5.
Goal
Finish the #88 migration by converting the schema panel's state — schema, schemaError, schemaFilter — to @preact/signals-core signals, so the tree repaints via an effect() like every other slice. The schema panel stays imperative (no Preact — the spike was rejected, ADR-0001 addendum); the in-place mutation that made this the awkward slice is replaced with reference-replacing updates, not a component model.
Files / scope
src/state.js — schema / schemaError / schemaFilter → signal(...).
src/ui/app.js — loadSchema / loadColumns write via .value; an effect() in createApp reads the schema signals and calls renderSchema(app) (replacing the scattered manual renderSchema calls); rebuildCompletions / updateBanner read .value; the schema-filter input sets schemaFilter.value.
src/ui/schema.js — renderSchema reads .value; expand toggles + lazy column load update by reference (see below), not in place.
- Tests — sweep
tests/unit/{schema,app,state}.test.js to .value (the unit pattern from the prior slices).
- Non-goals: not a Preact rewrite (rejected — ADR-0001 addendum); no schema-tree feature changes; the e2e editor harness is unaffected (no dep added).
Key implementation
- Mirror the established slice pattern (tabs / sidePanel / resultView / libraryName): field →
signal, reads/writes via .value, one effect() in createApp replaces manual renderX invalidation, batch() for multi-signal updates.
- Kill the in-place mutation (the reason this slice was awkward): replace the
expandedTables Set with a Set-valued signal updated immutably (expanded.value = new Set(expanded.value).add(key)); replace tb.columns in-place writes either with a Map-valued signal (cols.value = new Map(cols.value).set(key, …)) or by keeping tb.columns as a completion cache and bumping the schema signal. Pick the lower-churn option and document it in the PR.
- Keep
renderSchema imperative (rebuild the tree on the effect) — the documented exception to "no components" (ADR-0001 / CLAUDE.md rule 5).
Acceptance criteria
Re-evaluation trigger
If reference-replacing the tree proves as forgettable as the old manual renderSchema calls (the "relocated pain" the ADR warned about), reconsider whether the schema panel stays a documented imperative exception or warrants the (currently-rejected) component model — but only via a fresh ADR.
Tracking
Phase 1 of #68; the last slice of #88 (closes the migration). The rejected Preact alternative is preserved on spike/preact-schema as evidence.
Part of #68 (Phase 1) — the final slice of the #88 reactivity migration. Links to roadmap #68; foundation in
docs/ADR-0001-reactivity.md+ CLAUDE.md rule 5.Goal
Finish the #88 migration by converting the schema panel's state —
schema,schemaError,schemaFilter— to@preact/signals-coresignals, so the tree repaints via aneffect()like every other slice. The schema panel stays imperative (no Preact — the spike was rejected, ADR-0001 addendum); the in-place mutation that made this the awkward slice is replaced with reference-replacing updates, not a component model.Files / scope
src/state.js—schema/schemaError/schemaFilter→signal(...).src/ui/app.js—loadSchema/loadColumnswrite via.value; aneffect()increateAppreads the schema signals and callsrenderSchema(app)(replacing the scattered manualrenderSchemacalls);rebuildCompletions/updateBannerread.value; the schema-filter input setsschemaFilter.value.src/ui/schema.js—renderSchemareads.value; expand toggles + lazy column load update by reference (see below), not in place.tests/unit/{schema,app,state}.test.jsto.value(the unit pattern from the prior slices).Key implementation
signal, reads/writes via.value, oneeffect()increateAppreplaces manualrenderXinvalidation,batch()for multi-signal updates.expandedTablesSet with a Set-valued signal updated immutably (expanded.value = new Set(expanded.value).add(key)); replacetb.columnsin-place writes either with a Map-valued signal (cols.value = new Map(cols.value).set(key, …)) or by keepingtb.columnsas a completion cache and bumping theschemasignal. Pick the lower-churn option and document it in the PR.renderSchemaimperative (rebuild the tree on the effect) — the documented exception to "no components" (ADR-0001 / CLAUDE.md rule 5).Acceptance criteria
schema/schemaError/schemaFilteraresignal(...); all reads/writes go through.value.effect()repaints the tree (no leftover manualrenderSchemacalls beyond any deliberate data-path one).npm testgreen at the per-file 100% gate;npm run buildclean; e2e green on all three engines.CHANGELOG [Unreleased]+ Roadmap to 1.0.0 #68 Phase 1 reconciled; with this slice done, Adopt reactivity via @preact/signals-core, incrementally (ADR-0001) #88 can close.Re-evaluation trigger
If reference-replacing the tree proves as forgettable as the old manual
renderSchemacalls (the "relocated pain" the ADR warned about), reconsider whether the schema panel stays a documented imperative exception or warrants the (currently-rejected) component model — but only via a fresh ADR.Tracking
Phase 1 of #68; the last slice of #88 (closes the migration). The rejected Preact alternative is preserved on
spike/preact-schemaas evidence.