Skip to content

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

Open
melv-n wants to merge 2 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 2 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: 1cbfe65

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
@felipefreitag

Copy link
Copy Markdown
Member

Thanks for the PR @melv-n ! #3633 changed some of the hot reload logic, can you update your branch and check that part still works?

@melv-n melv-n force-pushed the feat/preview-props-editor branch from c22ed98 to 1cbfe65 Compare July 8, 2026 13:55
@melv-n

melv-n commented Jul 8, 2026

Copy link
Copy Markdown
Author

Thanks for the PR @melv-n ! #3633 changed some of the hot reload logic, can you update your branch and check that part still works?

Thanks for the heads up @felipefreitag! Rebased onto canary and reconciled with #3633.

The interesting overlap was the render cache. My branch made it props-aware (so editing props in the toolbar renders without clobbering the default), and #3633 changed hot reload to re-render only the open preview and just drop the cache for the other affected templates. I wired the two together:

  • invalidateEmailRenderingCache now clears every cached variant of a template (its defaults and any props overrides), so saving a shared component doesn't leave a stale override render behind.

  • The single re-render from planHotReloadRerender passes the active props override. Since the planner only ever hands back the currently open preview, the props you're editing survive a hot reload.

So the OOM fix stays intact and the props editor keeps working across saves. I extended the tests to cover it, and kept your new invalidateEmailRenderingCache and planHotReloadRerender cases alongside the props-override ones. Let me know if you'd like anything changed!

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.

2 participants