Skip to content

feat(ui): add props editor to the preview toolbar#3613

Open
melv-n wants to merge 3 commits into
resend:canaryfrom
melv-n:feat/preview-props-editor
Open

feat(ui): add props editor to the preview toolbar#3613
melv-n wants to merge 3 commits into
resend:canaryfrom
melv-n:feat/preview-props-editor

Conversation

@melv-n

@melv-n melv-n commented Jul 2, 2026

Copy link
Copy Markdown

Adds the preview props editing that was originally floated in #371 (that issue was closed once PreviewProps defaults landed, so this PR doesn't close anything; it builds the UI-editor half that was discussed there): a Props tab in the preview toolbar (next to Linter/Compatibility/Spam) that lets you live-edit the props a template is rendered with, to try out long values, different alphabets, edge cases, and so on, without touching the PreviewProps in the template file.

What it does

  • The Props tab shows a JSON editor seeded with the resolved props of the current render (the template's PreviewProps by default).
  • Edits apply live: valid JSON re-renders the preview through the existing renderEmailByPath server action (debounced 400ms). Invalid JSON shows the parse error inline and leaves the preview untouched.
  • "Reset to defaults" drops the override and re-renders with the template's own PreviewProps.
  • Hot reload keeps the override: saving the template file re-renders with your edited props.
  • The tab is hidden for raw .html templates (no props) and in static builds (isBuilding), where the server action does not exist.

Implementation notes

  • renderEmailByPath gains an optional previewPropsOverride parameter used instead of Email.PreviewProps, and the render cache now keys on path + override so default and overridden renders don't clobber each other (invalidatingCache clears every entry for the path). The returned metadata includes previewProps, the JSON-safe props the render used, which is what seeds the editor.
  • The override state lives in PreviewProvider and flows through useEmailRenderingResult, which re-renders on override changes and passes the current override to the hot-reload re-render (via a ref, so the socket listener isn't re-registered).
  • Props that are not JSON-serializable (functions, elements) can't round-trip through a JSON editor; previewProps falls back to {} for those templates and everything else keeps working.

How to test

  1. pnpm --filter=react-email dev a project (or the demo app), open a template preview
  2. Open the Props tab in the bottom toolbar, edit a value, watch the preview update
  3. Break the JSON, see the inline error; hit Reset to defaults
  4. Edit the template file while an override is active; the reload keeps your edited props

Unit coverage added in render-email-by-path.spec.ts for the override parameter, the exposed previewProps, and cache isolation between default and overridden renders. Full @react-email/ui suite passes (99 tests).

Screenshot

CleanShot 2026-07-02 at 16 44 37@2x

Related to #371.

@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c22ed98

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

This PR includes changesets to release 3 packages
Name Type
@react-email/ui Minor
react-email Minor
@react-email/editor 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

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@melv-n is attempting to deploy a commit to the resend Team on Vercel.

A member of the Team first needs to authorize it.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 8 files

Confidence score: 3/5

  • In packages/ui/src/components/toolbar.tsx, activeTab is taken from the URL without checking whether that tab is actually available when isRawHtmlEmail/isBuilding hide the Props trigger, so the UI can land on an invalid tab state and show missing/incorrect content to users — validate or coerce activeTab to a visible tab before rendering Tabs.Content and add a quick navigation test for hidden-tab scenarios.
  • In packages/ui/src/hooks/use-email-rendering-result.ts, the isFirstOverrideRun guard not resetting on the early unmount path can trigger an extra server action during React Strict Mode double-mount, which adds noise and avoidable backend work in development — reset the guard consistently in cleanup/early-return paths before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/ui/src/components/toolbar.tsx">

<violation number="1" location="packages/ui/src/components/toolbar.tsx:328">
P2: The Props tab trigger is conditionally hidden when `isRawHtmlEmail` or `isBuilding` is true, but `activeTab` comes directly from the URL query param without validating against available tabs, and `Tabs.Content value="props">` unconditionally renders `<PreviewPropsEditor />`. When a user navigates to `?toolbar-panel=props` in a raw HTML preview or static build, the editor renders and mutating the JSON triggers `useEmailRenderingResult` to call `renderEmailByPath` — a server action that is unavailable in static builds, causing a runtime failure. The content should be guarded the same way as the trigger, or `activeTab` should be validated against the supported tabs for the current mode.</violation>
</file>

<file name="packages/ui/src/hooks/use-email-rendering-result.ts">

<violation number="1" location="packages/ui/src/hooks/use-email-rendering-result.ts:26">
P3: The `isFirstOverrideRun` guard in the effect doesn't reset on unmount in its early-return path, which causes an unnecessary server action call in React Strict Mode's double-mount behavior. When navigating to a new email or during HMR in development, the ref persists across mounts, so the component re-enters the effect with `isFirstOverrideRun` already `false` and issues a redundant `renderEmailByPath(emailPath, false, undefined)` call that returns the same data already in initial state.

Returning a cleanup that resets the ref from the early-return path would prevent this.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/ui/src/components/toolbar.tsx Outdated
Comment thread packages/ui/src/hooks/use-email-rendering-result.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Re-trigger cubic

@melv-n melv-n marked this pull request as ready for review July 2, 2026 12:55
@github-actions github-actions Bot added the linear-synced PR has been synced to Linear label Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linear-synced PR has been synced to Linear

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant