Skip to content

fix: safe element merging via useRender#389

Merged
noahchoii merged 16 commits intomainfrom
create-default-element
Dec 22, 2025
Merged

fix: safe element merging via useRender#389
noahchoii merged 16 commits intomainfrom
create-default-element

Conversation

@noahchoii
Copy link
Contributor

@noahchoii noahchoii commented Dec 11, 2025

Description of Changes

1. Motivation

The existing create-slot utility functioned by defining a new component during the render phase. This caused the component reference to change on every re-render, leading to React identifying it as a different component during Reconciliation.

This resulted in several critical issues:

  • CSS Transitions Broken: Transitions failed to apply because the component was unmounted and remounted on every update.
  • State Reset: Instance state (e.g., an open state in a Popup) was lost because a fresh instance was created, causing the UI to behave unexpectedly (e.g., closing immediately or losing context).

2. Changes

I introduced a new utility, createDefaultElement, to replace the problematic logic.

  • Approach: Instead of defining a new component, it uses cloneElement to clone the existing element and merge props.
  • Result: This ensures reference stability, preventing unnecessary unmounts/remounts and resolving the transition and state issues mentioned above.

3. Trade-offs & Notes

1) Usage Change (JSX → Element)
Since the new utility returns a React Element instance rather than a Component definition, you must render the variable directly instead of using JSX syntax.

// AS-IS
const Child = createSlot(...);
return <Child />; // Used as a component

// TO-BE
const child = createDefaultElement(...);
return child; // Rendered directly

2) Style Handling in IconButton
To prevent unintended HTML attributes (like width or height) from being applied to non-SVG children (e.g., Text components), default dimensions are now applied via className instead of inline attributes.

  • Limitation: Because the size is defined via a CSS class, if a user attempts to override the size using HTML attributes (e.g., <HeartIcon width="36px" />), the CSS class may take precedence, and the inline attribute might be ignored.
  • Future Consideration: We may consider introducing a css prop object to handle style overrides more flexibly in the future.

Checklist

Before submitting the PR, please make sure you have checked all of the following items.

  • The PR title follows the Conventional Commits convention. (e.g., feat, fix, docs, style, refactor, test, chore)
  • I have added tests for my changes.
  • I have updated the Storybook or relevant documentation.
  • I have added a changeset for this change. (e.g., for any changes that affect users, such as component prop changes or new features).
  • I have performed a self-code review.
  • I have followed the project's coding conventions and component patterns.

@noahchoii noahchoii added type: bug Issues related to bug fixes scope: all Issues related to the all scope of component. labels Dec 11, 2025
@changeset-bot
Copy link

changeset-bot bot commented Dec 11, 2025

🦋 Changeset detected

Latest commit: 58bbc75

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

This PR includes changesets to release 5 packages
Name Type
@vapor-ui/hooks Patch
@vapor-ui/icons Patch
@vapor-ui/core Patch
figma-plugin Patch
website 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
Copy link
Contributor

vercel bot commented Dec 11, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
vapor-ui Ready Ready Preview, Comment Dec 15, 2025 0:08am

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @noahchoii, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses critical React reconciliation issues caused by the previous create-slot utility, which led to unstable component references, broken CSS transitions, and state loss on re-renders. The solution involves introducing a new createDefaultElement utility that leverages cloneElement to ensure element reference stability. This change impacts how elements are rendered and how styles are applied in components like IconButton, and also includes an upgrade of React dependencies to version 18.

Highlights

  • New Utility: createDefaultElement: Introduced a new utility, createDefaultElement, which replaces the problematic create-slot utility. This new utility uses cloneElement to clone existing elements and merge props, ensuring reference stability.
  • Resolved React Reconciliation Issues: The previous create-slot utility caused component references to change on every re-render, leading to React identifying them as different components during reconciliation. This PR fixes issues like broken CSS transitions and state resets by ensuring stable references.
  • Usage Change for Element Rendering: The new createDefaultElement returns a React Element instance, requiring direct rendering of the variable instead of using JSX component syntax (e.g., return child; instead of return <Child />;).
  • Improved IconButton Style Handling: Default dimensions for IconButton are now applied via className instead of inline HTML attributes. This prevents unintended attributes (like width or height) from being applied to non-SVG children, though it introduces a limitation where CSS classes may take precedence over inline attribute overrides.
  • React Version Upgrade: The project's React and ReactDOM dependencies have been upgraded from version 17 to 18, along with their corresponding TypeScript type definitions.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@noahchoii
Copy link
Contributor Author

@SimYunSup Since it seems similar to what you worked on in this PR, I'm leaving a comment!

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new createDefaultElement utility to enhance component customizability by allowing ReactNode children to be cloned and augmented with default props, replacing the previous createSlot utility. This change impacts numerous components across @vapor-ui/core, including Breadcrumb, Checkbox, Dialog, IconButton, Menu, MultiSelect, NavigationMenu, Pagination, Popover, Radio, Select, Sheet, Switch, and Toast. Specifically, it refactors how default icons and nested elements are rendered, enabling users to provide custom React elements while retaining default styling and behavior. For instance, IconButton and Checkbox now support arbitrary children, and Toast icons can be customized. Additionally, the styling for IconButton and Pagination icons has been updated to use dynamic sizing based on parent elements, and the project's React dependencies have been upgraded from v17 to v18, along with corresponding @types/react and @types/react-dom updates.

@vapor-ui
Copy link
Collaborator

vapor-ui commented Dec 11, 2025

All tests passed!

Tests Passed Failed Duration Report
136 136 0 1m 15s Open report ↗︎

Click here if you need to update snapshots.

@SimYunSup
Copy link
Contributor

SimYunSup commented Dec 11, 2025

If createDefaultElement needs for cloning elements and adding props at cloned elements, how about using useRender instead of create-slot and createDefaultElement?

@noahchoii
Copy link
Contributor Author

@SimYunSup

I had initially thought that creating a custom utility function would be preferable to using useRender in terms of code conciseness and type inference.

However, I reconsidered and changed it because I realized there are advantages to using useRender—such as avoiding multiple functions with similar roles and ensuring stability. This made me rethink things once again. Thank you!

@SimYunSup SimYunSup mentioned this pull request Dec 16, 2025
6 tasks
@noahchoii noahchoii changed the title fix: safe element merging via create-default-element fix: safe element merging via useRender Dec 18, 2025
Comment on lines +102 to +109
props: {
children: (
<>
{overlay}
{popup}
</>
),
},
Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM

NOTE: Due to the internal implementation of useRender, where the props merge order is mergeProps(props, render.props), if children are included in portalElement or overlayElement, the children injected internally may be overwritten.

<DialogPopup portalElement={<CustomPortal>something</CustomPortal>} />

However, this is how useRender works, and since there's almost no use case for directly passing children to Portal, it's probably best to just acknowledge this and move on.

@noahchoii noahchoii merged commit 8c5c755 into main Dec 22, 2025
13 checks passed
@noahchoii noahchoii deleted the create-default-element branch December 22, 2025 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: all Issues related to the all scope of component. type: bug Issues related to bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug(Sheet): Uncontrolled tab in sheet is reinitializing when re-render

4 participants