Skip to content

fix: map vectorEffect and keep SVG context in streamed Suspense - #122

Draft
azizbecha wants to merge 1 commit into
TanStack:mainfrom
azizbecha:fix/svg-vector-effect-streamed-suspense
Draft

azizbecha wants to merge 1 commit into
TanStack:mainfrom
azizbecha:fix/svg-vector-effect-streamed-suspense

Conversation

@azizbecha

Copy link
Copy Markdown

Closes #121.

Summary

Two gaps remained after #76 kept SVG names as written:

  • vectorEffect was never mapped to vector-effect. The browser ignored it. On server-rendered pages the HTML parser also lowercased it to vectoreffect, so hydration failed to find vectorEffect and discarded the boundary. attributeName() now hyphenates it.
  • Suspense boundaries streamed directly inside <svg> lost their SVG context. The content was walked with the SVG flag off, which lowercased preserveAspectRatio. The <div> placeholder also made the parser break out of the <svg>, and the segment was parsed in an HTML <div>, so the content ended up outside the <svg> in the HTML namespace.

For the second gap, SuspendedBoundary now records the parent's SVG flag and restores it when the boundary streams. Inside SVG, the placeholder is a <g> and the staging element is <svg style="display:none">. Everywhere else the wire format is unchanged, including inside <foreignObject>. The reveal runtime and client hydration needed no changes because they find both elements by id.

Decisions for maintainers

Size budgets. Cost against main, in gzip bytes:

Bundle Source Built
redact/server +59 +62
redact/dom-client (hydration=stub) +6 +7
  • scripts/size-check.mjs gets a SVG_CONTEXT_SIZE_ALLOWANCE table for the amount over budget. It follows the existing allowance tables, and is yours to accept or reject.
  • scripts/test-react-compatibility-size.mjs will fail. I left compatibility-size-budget.mjs alone because it records approved growth per baseline. Growth is 5–6 B gzip on client entries and 70 B gzip on server/index.

Overlap with open PRs.

I read both diffs but did not run either PR.

Testing

New cases in tests/svg-attribute-casing.test.tsx:

  • vectorEffect through SSR, hydration and createRoot
  • a boundary streamed inside <svg>, revealed and then hydrated
  • hydration that starts before the reveal arrives
  • a <foreignObject> boundary staying in HTML context

The vectorEffect and streamed <svg> cases failed before the fix. The pending-hydration case was added afterwards and was not run against main. The <foreignObject> case guards existing behavior and also passed before the fix.

  • Test suite and types: the full suite and pnpm test:types pass on Node 24.21.0. The full suite has one failure on Node 25, in hydration-mismatch-recovery.test.tsx, which also fails on main and passes with --no-webstorage.
  • Size checks: .nvmrc pins 24.22.0, which I didn't have, so I ran a copy of size-check.mjs with the version guard removed on 24.21.0. main measured under budget on that version, with the hydration stub exactly at its limit, but CI on 24.22.0 is the real result.
  • Real browser (headless Chrome 154, compared with React 19.3.1):
    • vectorEffect now applies and hydrates cleanly.
    • Streamed content lands inside the <svg>, in the SVG namespace, with preserveAspectRatio honored.

Known limits

  • Not tested in Firefox or Safari.
  • A pending fallback inside <text> is wrapped in a <g>, which SVG doesn't render there, so that fallback won't show until the reveal.
  • <title> and <desc> inside <svg> are HTML integration points. Boundaries directly inside them still get the SVG treatment, and I didn't test that case.
  • The staging <svg> relies on an inline style attribute, so a strict style-src CSP would block it and leave the content visible until the reveal.

@changeset-bot

changeset-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 64eb489

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/redact Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 31, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

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.

SVG: vectorEffect breaks hydration, and streamed Suspense inside <svg> lowercases attributes

1 participant