Skip to content

feat(ui,ui-server)!: remove Suspense — loading UX is early-return + query.loading [#2985]#2990

Merged
viniciusdacal merged 3 commits intomainfrom
fix/remove-suspense
Apr 23, 2026
Merged

feat(ui,ui-server)!: remove Suspense — loading UX is early-return + query.loading [#2985]#2990
viniciusdacal merged 3 commits intomainfrom
fix/remove-suspense

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

Closes #2985.

  • Suspense is rejected as a Vertz primitive. Vertz's signal/reactivity model already handles loading states via query().loading plus compiler-supported early-return guards (if (q.loading) return <Loading/>; return <Real/>) — which also keeps q.data typed-defined past the guard so APIs like form(..., { initial: () => q.data }) work without null checks.
  • The Suspense we shipped was half-wired. The Suspense component existed in @vertz/ui but had no JSX-compatible children contract (children: () => JsxElement vs how the compiler wraps children). It was never used as JSX anywhere in the repo and never documented in mint-docs. The parallel __suspense VNode streaming protocol in @vertz/ui-server (SuspenseVNode, createSlotPlaceholder, createTemplateChunk) was dead code — nothing in shipped code produced __suspense nodes. Both are gone.
  • Loading docs updated. New "Early return when you need loaded data" section in mint-docs/guides/ui/data-fetching.mdx documenting the replacement pattern with a form-on-loaded-data example and the flat-guard constraint.

Public API Changes

Breaking removals from @vertz/ui:

  • Suspense component
  • SuspenseProps type

Breaking removals from @vertz/ui-server:

  • createSlotPlaceholder
  • resetSlotCounter
  • createTemplateChunk
  • RenderToStreamOptions (and the nonce option on renderToStream)
  • renderToStream(tree, options?) is now renderToStream(tree) — fully synchronous, single HTML chunk.

Internal cleanup:

  • error-boundary-context.ts (the pushErrorHandler/popErrorHandler/getCurrentErrorHandler stack used only by Suspense) removed. ErrorBoundary keeps its synchronous try/catch + retry unchanged.

Commits

  • 057b1607a — main removal
  • 8528eee17 — adversarial-review follow-up: drop stale nonce Options block + CSP Nonce Support section from @vertz/ui-server README
  • 87329972a — oxfmt fix

Adversarial Review

  • Review written to reviews/remove-suspense/phase-01-removal.md — one BLOCKER found (stale README docs), fixed in 8528eee17.

Test plan

  • vtz test on @vertz/ui — 2461 passed
  • vtz test on @vertz/ui-server — 1270 passed / 60 skipped / 15 todo
  • tsgo --noEmit on both packages — clean
  • oxlint — 0 errors
  • oxfmt --check — clean
  • cargo test --all — 47 passed
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • Full bun run ci:build-typecheck:affected — 40/40 pass
  • Repo-wide grep: no remaining Suspense/__suspense/SuspenseVNode/SuspenseProps/createSlotPlaceholder/resetSlotCounter/createTemplateChunk/slot-placeholder/template-chunk/error-boundary-context/RenderToStreamOptions references outside CHANGELOGs, plans/, and the changeset file itself.

🤖 Generated with Claude Code

viniciusdacal and others added 3 commits April 22, 2026 21:53
…uery.loading [#2985]

Closes #2985.

Suspense is rejected as a Vertz primitive. The signal/reactivity model already
handles loading states via `query().loading` plus the compiler-supported
early-return guard pattern (`if (q.loading) return <Loading/>; return <Real/>`),
which also keeps `q.data` typed-defined past the guard so APIs like
`form(..., { initial: () => q.data })` work without null checks. The Promise-
throwing Suspense machinery that mirrored React added no capability that Vertz
didn't already have, and coupled user-facing code to a runtime contract the
compiler doesn't model.

Removed

- packages/ui: `Suspense` component, `SuspenseProps` type, and the
  internal `error-boundary-context` module (async-error handler stack used
  only by Suspense). `ErrorBoundary` keeps its synchronous try/catch + retry
  behavior; redundant async-path tests are gone.
- packages/ui-server: `SuspenseVNode` / `__suspense` tag and its deferred
  resolution loop in `renderToStream`. `renderToStream(tree, options?)` is
  now `renderToStream(tree)` — walks the tree synchronously and emits a
  single HTML chunk. `createSlotPlaceholder`, `resetSlotCounter`,
  `createTemplateChunk`, and `RenderToStreamOptions` are removed along with
  their tests. Nothing in shipped code produced `__suspense` VNodes.
- Docs: `@vertz/ui` README / api-cheat-sheet, `@vertz/ui-server` README, and
  `docs/guides/ssr-hydration.md` no longer mention Suspense. A new section
  ("Early return when you need loaded data") in
  `mint-docs/guides/ui/data-fetching.mdx` documents the replacement pattern.

Public API changes

- Breaking: `Suspense`, `SuspenseProps` removed from `@vertz/ui`.
- Breaking: `createSlotPlaceholder`, `resetSlotCounter`, `createTemplateChunk`,
  `RenderToStreamOptions` removed from `@vertz/ui-server`.
- Breaking: `renderToStream` no longer accepts an options argument.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion [#2985]

Review follow-up: the Options bullet under `renderToStream(tree)` referenced
the removed `nonce?: string` field, and the "CSP Nonce Support" section showed
`renderToStream(tree, { nonce })` which is no longer a valid call signature
after Suspense removal dropped `RenderToStreamOptions`. Both blocks are gone.

Other nonce-aware APIs (ssr-progressive-response, ssr-access-set,
template-inject) are unaffected and retain their nonce parameters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@viniciusdacal viniciusdacal merged commit e84adde into main Apr 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compiler does not transform multi-statement arrow function children inside <Suspense>

1 participant