diff --git a/.changeset/stale-dryers-speak.md b/.changeset/stale-dryers-speak.md new file mode 100644 index 000000000..04530b41e --- /dev/null +++ b/.changeset/stale-dryers-speak.md @@ -0,0 +1,5 @@ +--- +'@vapor-ui/core': patch +--- + +Change createSlot to memoize diff --git a/packages/core/src/components/breadcrumb/breadcrumb.tsx b/packages/core/src/components/breadcrumb/breadcrumb.tsx index af34a9aa0..c69b991ac 100644 --- a/packages/core/src/components/breadcrumb/breadcrumb.tsx +++ b/packages/core/src/components/breadcrumb/breadcrumb.tsx @@ -6,8 +6,8 @@ import { useRender } from '@base-ui-components/react'; import { MoreCommonOutlineIcon, SlashOutlineIcon } from '@vapor-ui/icons'; import clsx from 'clsx'; +import { useSlot } from '~/hooks/use-slot'; import { createContext } from '~/libs/create-context'; -import { createSlot } from '~/libs/create-slot'; import { createSplitProps } from '~/utils/create-split-props'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VComponentProps } from '~/utils/types'; @@ -152,7 +152,7 @@ export const BreadcrumbSeparator = forwardRef); + const IconElement = useSlot(children, ); return useRender({ ref, @@ -180,7 +180,7 @@ export const BreadcrumbEllipsisPrimitive = forwardRef< const { render, className, children, ...componentProps } = resolveStyles(props); const { size } = useBreadcrumbContext(); - const IconElement = createSlot(children || ); + const IconElement = useSlot(children, ); return useRender({ ref, diff --git a/packages/core/src/components/checkbox/checkbox.tsx b/packages/core/src/components/checkbox/checkbox.tsx index 9889f6340..97d2a0629 100644 --- a/packages/core/src/components/checkbox/checkbox.tsx +++ b/packages/core/src/components/checkbox/checkbox.tsx @@ -6,8 +6,8 @@ import { forwardRef } from 'react'; import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox'; import clsx from 'clsx'; +import { useSlot } from '~/hooks/use-slot'; import { createContext } from '~/libs/create-context'; -import { createSlot } from '~/libs/create-slot'; import { createSplitProps } from '~/utils/create-split-props'; import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; @@ -40,7 +40,7 @@ export const CheckboxRoot = forwardRef((p const { size, invalid, indeterminate } = variantProps; const dataAttrs = createDataAttributes({ invalid }); - const IndicatorElement = createSlot(children || ); + const IndicatorElement = useSlot(children, ); return ( diff --git a/packages/core/src/components/dialog/dialog.tsx b/packages/core/src/components/dialog/dialog.tsx index 10f7dbd5f..972514a47 100644 --- a/packages/core/src/components/dialog/dialog.tsx +++ b/packages/core/src/components/dialog/dialog.tsx @@ -7,8 +7,8 @@ import { Dialog as BaseDialog } from '@base-ui-components/react/dialog'; import { useRender } from '@base-ui-components/react/use-render'; import clsx from 'clsx'; +import { useSlot } from '~/hooks/use-slot'; import { createContext } from '~/libs/create-context'; -import { createSlot } from '~/libs/create-slot'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VComponentProps } from '~/utils/types'; @@ -92,8 +92,8 @@ DialogPopupPrimitive.displayName = 'Dialog.PopupPrimitive'; export const DialogPopup = forwardRef((props, ref) => { const { portalElement, overlayElement, ...componentProps } = resolveStyles(props); - const PortalElement = createSlot(portalElement || ); - const DialogOverlayPrimitiveElement = createSlot(overlayElement || ); + const PortalElement = useSlot(portalElement, ); + const DialogOverlayPrimitiveElement = useSlot(overlayElement, ); return ( diff --git a/packages/core/src/components/icon-button/icon-button.tsx b/packages/core/src/components/icon-button/icon-button.tsx index 254ad41b7..d850e91e5 100644 --- a/packages/core/src/components/icon-button/icon-button.tsx +++ b/packages/core/src/components/icon-button/icon-button.tsx @@ -1,8 +1,8 @@ -import { forwardRef, useMemo } from 'react'; +import { forwardRef } from 'react'; import clsx from 'clsx'; -import { createSlot } from '~/libs/create-slot'; +import { useSlot } from '~/hooks/use-slot'; import { createSplitProps } from '~/utils/create-split-props'; import { resolveStyles } from '~/utils/resolve-styles'; import type { VComponentProps } from '~/utils/types'; @@ -25,7 +25,7 @@ export const IconButton = forwardRef((props const { size } = otherProps; - const IconElement = useMemo(() => createSlot(children), [children]); + const IconElement = useSlot(children); return ( + + 3 + 4 + + + + + ); +}; diff --git a/packages/core/src/components/sheet/sheet.tsx b/packages/core/src/components/sheet/sheet.tsx index 4d292e65f..830bdde31 100644 --- a/packages/core/src/components/sheet/sheet.tsx +++ b/packages/core/src/components/sheet/sheet.tsx @@ -9,10 +9,10 @@ import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; import clsx from 'clsx'; import { useOpenChangeComplete } from '~/hooks/use-open-change-complete'; +import { useSlot } from '~/hooks/use-slot'; import type { TransitionStatus } from '~/hooks/use-transition-status'; import { useTransitionStatus } from '~/hooks/use-transition-status'; import { createContext } from '~/libs/create-context'; -import { createSlot } from '~/libs/create-slot'; import { composeRefs } from '~/utils/compose-refs'; import { createSplitProps } from '~/utils/create-split-props'; import { createDataAttributes } from '~/utils/data-attributes'; @@ -190,9 +190,9 @@ SheetPopupPrimitive.displayName = 'Sheet.PopupPrimitive'; export const SheetPopup = forwardRef( ({ portalElement, overlayElement, positionerElement, ...props }, ref) => { - const PortalElement = createSlot(portalElement || ); - const OverlayElement = createSlot(overlayElement || ); - const PositionerElement = createSlot(positionerElement || ); + const PortalElement = useSlot(portalElement, ); + const OverlayElement = useSlot(overlayElement, ); + const PositionerElement = useSlot(positionerElement, ); return ( diff --git a/packages/core/src/components/switch/switch.tsx b/packages/core/src/components/switch/switch.tsx index 318451eb7..da7deb46f 100644 --- a/packages/core/src/components/switch/switch.tsx +++ b/packages/core/src/components/switch/switch.tsx @@ -5,8 +5,8 @@ import { forwardRef, useMemo } from 'react'; import { Switch as BaseSwitch } from '@base-ui-components/react'; import clsx from 'clsx'; +import { useSlot } from '~/hooks/use-slot'; import { createContext } from '~/libs/create-context'; -import { createSlot } from '~/libs/create-slot'; import { createSplitProps } from '~/utils/create-split-props'; import { createDataAttributes } from '~/utils/data-attributes'; import { resolveStyles } from '~/utils/resolve-styles'; @@ -40,7 +40,7 @@ export const SwitchRoot = forwardRef((props const dataAttrs = createDataAttributes({ invalid }); - const ThumbElement = useMemo(() => createSlot(), []); + const ThumbElement = useSlot(useMemo(() => , [])); const children = childrenProp || ; return ( diff --git a/packages/core/src/components/tooltip/tooltip.tsx b/packages/core/src/components/tooltip/tooltip.tsx index 37435027a..f192959c3 100644 --- a/packages/core/src/components/tooltip/tooltip.tsx +++ b/packages/core/src/components/tooltip/tooltip.tsx @@ -7,7 +7,7 @@ import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip'; import clsx from 'clsx'; import { useMutationObserver } from '~/hooks/use-mutation-observer'; -import { createSlot } from '~/libs/create-slot'; +import { useSlot } from '~/hooks/use-slot'; import { vars } from '~/styles/themes.css'; import { composeRefs } from '~/utils/compose-refs'; import { resolveStyles } from '~/utils/resolve-styles'; @@ -155,9 +155,10 @@ const extractPositions = (dataset: DOMStringMap) => { export const TooltipPopup = forwardRef( ({ portalElement, positionerElement, ...props }, ref) => { - const PortalElement = createSlot(portalElement || ); - const PositionerElement = createSlot( - positionerElement || , + const PortalElement = useSlot(portalElement, ); + const PositionerElement = useSlot( + positionerElement, + , ); return ( diff --git a/packages/core/src/hooks/use-slot.ts b/packages/core/src/hooks/use-slot.ts new file mode 100644 index 000000000..9fae19e7a --- /dev/null +++ b/packages/core/src/hooks/use-slot.ts @@ -0,0 +1,39 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { Children, Fragment, cloneElement, forwardRef, isValidElement, useMemo } from 'react'; + +import { composeRefs } from '~/utils/compose-refs'; +import { getElementRef } from '~/utils/get-element-ref'; +import { mergeProps } from '~/utils/merge-props'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Any = any; + +interface SlotProps extends HTMLAttributes {} + +export const useSlot = ( + _children: ReactNode, + fallback?: ReactNode, +) => { + const Slot = useMemo( + () => + forwardRef((slotProps, forwardedRef) => { + const children = _children || fallback; + if (!isValidElement(children)) { + return Children.count(children) > 1 ? Children.only(null) : null; + } + + const childrenRef = getElementRef(children); + const props = mergeProps(slotProps || {}, children.props); + + if (children.type !== Fragment && children.props.ref) { + props.ref = composeRefs(forwardedRef, childrenRef); + } + + return cloneElement(children, props); + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [_children], + ); + + return Slot; +}; diff --git a/packages/core/src/libs/create-slot.ts b/packages/core/src/libs/create-slot.ts deleted file mode 100644 index 862d6d893..000000000 --- a/packages/core/src/libs/create-slot.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { HTMLAttributes, ReactNode } from 'react'; -import { Children, Fragment, cloneElement, forwardRef, isValidElement } from 'react'; - -import { composeRefs } from '~/utils/compose-refs'; -import { getElementRef } from '~/utils/get-element-ref'; -import { mergeProps } from '~/utils/merge-props'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Any = any; - -interface SlotProps extends HTMLAttributes {} - -export const createSlot = (children: ReactNode) => { - const Slot = forwardRef((slotProps, forwardedRef) => { - if (!isValidElement(children)) { - return Children.count(children) > 1 ? Children.only(null) : null; - } - - const childrenRef = getElementRef(children); - const props = mergeProps(slotProps || {}, children.props); - - if (children.type !== Fragment && children.props.ref) { - props.ref = composeRefs(forwardedRef, childrenRef); - } - - return cloneElement(children, props); - }); - - return Slot; -};