Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/stale-dryers-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vapor-ui/core': patch
---

Change createSlot to memoize
6 changes: 3 additions & 3 deletions packages/core/src/components/breadcrumb/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -152,7 +152,7 @@ export const BreadcrumbSeparator = forwardRef<HTMLLIElement, BreadcrumbSeparator
const { render, className, children, ...componentProps } = resolveStyles(props);

const { size } = useBreadcrumbContext();
const IconElement = createSlot(children || <SlashOutlineIcon size="100%" />);
const IconElement = useSlot(children, <SlashOutlineIcon size="100%" />);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

useSlot에 인라인 JSX를 fallback 인자로 전달하면 useSlot 내부의 useMemo가 매번 다시 실행되어 메모이제이션이 깨집니다. 이는 이 PR의 목적인 성능 최적화에 반하는 동작입니다.

fallback으로 사용되는 <SlashOutlineIcon size="100%" />는 props가 변경되지 않으므로, 컴포넌트 외부에 상수로 선언하여 useSlot에 전달하면 이 문제를 해결할 수 있습니다.

// 예시
const slashIcon = <SlashOutlineIcon size="100%" />;

export const BreadcrumbSeparator = forwardRef<HTMLLIElement, BreadcrumbSeparator.Props>(
    (props, ref) => {
        // ...
        const IconElement = useSlot(children, slashIcon);
        // ...
    },
);

이 패턴은 이 파일의 183번째 줄과 다른 컴포넌트 파일들에서도 동일하게 발견됩니다. 모든 useSlot 사용 부분을 검토하여 인라인 JSX 대신 안정적인 참조를 사용하도록 수정해주세요.


return useRender({
ref,
Expand Down Expand Up @@ -180,7 +180,7 @@ export const BreadcrumbEllipsisPrimitive = forwardRef<
const { render, className, children, ...componentProps } = resolveStyles(props);

const { size } = useBreadcrumbContext();
const IconElement = createSlot(children || <MoreCommonOutlineIcon size="100%" />);
const IconElement = useSlot(children, <MoreCommonOutlineIcon size="100%" />);

return useRender({
ref,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -40,7 +40,7 @@ export const CheckboxRoot = forwardRef<HTMLButtonElement, CheckboxRoot.Props>((p
const { size, invalid, indeterminate } = variantProps;
const dataAttrs = createDataAttributes({ invalid });

const IndicatorElement = createSlot(children || <CheckboxIndicatorPrimitive />);
const IndicatorElement = useSlot(children, <CheckboxIndicatorPrimitive />);

return (
<CheckboxProvider value={{ size, indeterminate }}>
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -92,8 +92,8 @@ DialogPopupPrimitive.displayName = 'Dialog.PopupPrimitive';
export const DialogPopup = forwardRef<HTMLDivElement, DialogPopup.Props>((props, ref) => {
const { portalElement, overlayElement, ...componentProps } = resolveStyles(props);

const PortalElement = createSlot(portalElement || <DialogPortalPrimitive />);
const DialogOverlayPrimitiveElement = createSlot(overlayElement || <DialogOverlayPrimitive />);
const PortalElement = useSlot(portalElement, <DialogPortalPrimitive />);
const DialogOverlayPrimitiveElement = useSlot(overlayElement, <DialogOverlayPrimitive />);

return (
<PortalElement>
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/components/icon-button/icon-button.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,7 +25,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButton.Props>((props

const { size } = otherProps;

const IconElement = useMemo(() => createSlot(children), [children]);
const IconElement = useSlot(children);

return (
<Button
Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Menu as BaseMenu } from '@base-ui-components/react';
import { ChevronRightOutlineIcon, ConfirmOutlineIcon } 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 { composeRefs } from '~/utils/compose-refs';
import { resolveStyles } from '~/utils/resolve-styles';
import type { VComponentProps } from '~/utils/types';
Expand Down Expand Up @@ -104,8 +104,8 @@ MenuPopupPrimitive.displayName = 'Menu.PopupPrimitive';

export const MenuPopup = forwardRef<HTMLDivElement, MenuPopup.Props>(
({ portalElement, positionerElement, ...props }, ref) => {
const PortalElement = createSlot(portalElement || <MenuPortalPrimitive />);
const PositionerElement = createSlot(positionerElement || <MenuPositionerPrimitive />);
const PortalElement = useSlot(portalElement, <MenuPortalPrimitive />);
const PositionerElement = useSlot(positionerElement, <MenuPositionerPrimitive />);

return (
<PortalElement>
Expand Down Expand Up @@ -270,9 +270,10 @@ MenuSubmenuPopupPrimitive.displayName = 'Menu.SubmenuPopupPrimitive';

export const MenuSubmenuPopup = forwardRef<HTMLDivElement, MenuSubmenuPopup.Props>(
({ portalElement, positionerElement, ...props }, ref) => {
const PortalElement = createSlot(portalElement || <MenuPortalPrimitive />);
const PositionerElement = createSlot(
positionerElement || <MenuPositionerPrimitive side="right" sideOffset={0} />,
const PortalElement = useSlot(portalElement, <MenuPortalPrimitive />);
const PositionerElement = useSlot(
positionerElement,
<MenuPositionerPrimitive side="right" sideOffset={0} />,
);

return (
Expand Down
12 changes: 5 additions & 7 deletions packages/core/src/components/multi-select/multi-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Select as BaseSelect } from '@base-ui-components/react';
import { ChevronDownOutlineIcon, ConfirmOutlineIcon } 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 { createDataAttributes } from '~/utils/data-attributes';
import { resolveStyles } from '~/utils/resolve-styles';
Expand Down Expand Up @@ -181,7 +181,7 @@ export const MultiSelectTriggerIconPrimitive = forwardRef<
const { className, children, ...componentProps } = resolveStyles(props);

const { size } = useMultiSelectContext();
const IconElement = createSlot(children || <ChevronDownOutlineIcon size="100%" />);
const IconElement = useSlot(children, <ChevronDownOutlineIcon size="100%" />);

return (
<BaseSelect.Icon
Expand Down Expand Up @@ -273,10 +273,8 @@ MultiSelectPopupPrimitive.displayName = 'MultiSelect.PopupPrimitive';

export const MultiSelectPopup = forwardRef<HTMLDivElement, MultiSelectPopup.Props>(
({ portalElement, positionerElement, ...props }, ref) => {
const PortalElement = createSlot(portalElement || <MultiSelectPortalPrimitive />);
const PositionerElement = createSlot(
positionerElement || <MultiSelectPositionerPrimitive />,
);
const PortalElement = useSlot(portalElement, <MultiSelectPortalPrimitive />);
const PositionerElement = useSlot(positionerElement, <MultiSelectPositionerPrimitive />);

return (
<PortalElement>
Expand Down Expand Up @@ -318,7 +316,7 @@ export const MultiSelectItemIndicatorPrimitive = forwardRef<
>((props, ref) => {
const { className, children, ...componentProps } = resolveStyles(props);

const IconElement = createSlot(children || <ConfirmOutlineIcon size="100%" />);
const IconElement = useSlot(children, <ConfirmOutlineIcon size="100%" />);

return (
<BaseSelect.ItemIndicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { ChevronDownOutlineIcon } from '@vapor-ui/icons';
import clsx from 'clsx';

import { useMutationObserver } from '~/hooks/use-mutation-observer';
import { useSlot } from '~/hooks/use-slot';
import { createContext } from '~/libs/create-context';
import { createSlot } from '~/libs/create-slot';
import { vars } from '~/styles/themes.css';
import { composeRefs } from '~/utils/compose-refs';
import { createSplitProps } from '~/utils/create-split-props';
Expand Down Expand Up @@ -167,7 +167,7 @@ export const NavigationMenuTriggerIndicatorPrimitive = forwardRef<
>((props, ref) => {
const { className, children, ...componentProps } = resolveStyles(props);

const IconElement = createSlot(children || <ChevronDownOutlineIcon />);
const IconElement = useSlot(children, <ChevronDownOutlineIcon />);

return (
<BaseNavigationMenu.Icon
Expand Down Expand Up @@ -403,11 +403,9 @@ NavigationMenuViewportPrimitive.displayName = 'NavigationMenu.ViewportPrimitive'

export const NavigationMenuViewport = forwardRef<HTMLDivElement, NavigationMenuViewport.Props>(
({ portalElement, positionerElement, popupElement, className, ...props }, ref) => {
const PortalElement = createSlot(portalElement ?? <NavigationMenuPortalPrimitive />);
const PopupElement = createSlot(popupElement ?? <NavigationMenuPopupPrimitive />);
const PositionerElement = createSlot(
positionerElement ?? <NavigationMenuPositionerPrimitive />,
);
const PortalElement = useSlot(portalElement, <NavigationMenuPortalPrimitive />);
const PopupElement = useSlot(popupElement, <NavigationMenuPopupPrimitive />);
const PositionerElement = useSlot(positionerElement, <NavigationMenuPositionerPrimitive />);

return (
<PortalElement>
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Popover as BasePopover } from '@base-ui-components/react/popover';
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';
Expand Down Expand Up @@ -157,8 +157,8 @@ const extractPositions = (dataset: DOMStringMap) => {

export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopup.Props>(
({ portalElement, positionerElement, ...props }, ref) => {
const PortalElement = createSlot(portalElement || <PopoverPortalPrimitive />);
const PositionerElement = createSlot(positionerElement || <PopoverPositionerPrimitive />);
const PortalElement = useSlot(portalElement, <PopoverPortalPrimitive />);
const PositionerElement = useSlot(positionerElement, <PopoverPositionerPrimitive />);

return (
<PortalElement>
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/radio/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { forwardRef } from 'react';
import { Radio as BaseRadio } from '@base-ui-components/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 { createDataAttributes } from '~/utils/data-attributes';
import { resolveStyles } from '~/utils/resolve-styles';
Expand Down Expand Up @@ -36,7 +36,7 @@ export const RadioRoot = forwardRef<HTMLButtonElement, RadioRoot.Props>((props,

const dataAttrs = createDataAttributes({ invalid });

const IndicatorElement = createSlot(children || <RadioIndicatorPrimitive />);
const IndicatorElement = useSlot(children, <RadioIndicatorPrimitive />);

return (
<BaseRadio.Root
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Select as BaseSelect } from '@base-ui-components/react';
import { ChevronDownOutlineIcon, ConfirmOutlineIcon } 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 { createDataAttributes } from '~/utils/data-attributes';
import { resolveStyles } from '~/utils/resolve-styles';
Expand Down Expand Up @@ -153,7 +153,7 @@ export const SelectTriggerIconPrimitive = forwardRef<

const { size } = useSelectContext();

const IconElement = createSlot(children || <ChevronDownOutlineIcon size="100%" />);
const IconElement = useSlot(children, <ChevronDownOutlineIcon size="100%" />);

return (
<BaseSelect.Icon
Expand Down Expand Up @@ -247,8 +247,8 @@ SelectPopupPrimitive.displayName = 'Select.PopupPrimitive';

export const SelectPopup = forwardRef<HTMLDivElement, SelectPopup.Props>(
({ portalElement, positionerElement, ...props }, ref) => {
const PortalElement = createSlot(portalElement || <SelectPortalPrimitive />);
const PositionerElement = createSlot(positionerElement || <SelectPositionerPrimitive />);
const PortalElement = useSlot(portalElement, <SelectPortalPrimitive />);
const PositionerElement = useSlot(positionerElement, <SelectPositionerPrimitive />);

return (
<PortalElement>
Expand Down Expand Up @@ -289,7 +289,7 @@ export const SelectItemIndicatorPrimitive = forwardRef<
SelectItemIndicatorPrimitive.Props
>((props, ref) => {
const { className, children, ...componentProps } = resolveStyles(props);
const IconElement = createSlot(children || <ConfirmOutlineIcon />);
const IconElement = useSlot(children, <ConfirmOutlineIcon />);

return (
<BaseSelect.ItemIndicator
Expand Down
49 changes: 49 additions & 0 deletions packages/core/src/components/sheet/sheet.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useState } from 'react';

import { cleanup, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'vitest-axe';

import { Sheet } from '.';
import { Tabs } from '../tabs';

describe('Sheet', () => {
const consoleWarnMockFunction = vi.spyOn(console, 'warn').mockImplementation(vi.fn());
Expand Down Expand Up @@ -154,6 +157,22 @@ describe('Sheet', () => {
expect(popup).toBeInTheDocument();
expect(popup).not.toBeVisible();
});

it('should keeping value tab in sheet when rerender', async () => {
const rendered = render(<RerenderSheetTest />);
const trigger = rendered.getByText(TRIGGER_TEXT);

// Open Sheet
await userEvent.click(trigger);
const packageTab = rendered.getByRole('tab', { name: 'Package' });

// Switch to Package tab
await userEvent.click(packageTab);
const packagePanel = rendered.getByTestId('package-panel');
expect(packagePanel).toBeVisible();
await userEvent.click(rendered.getByTestId('package-button'));
expect(packagePanel).toBeVisible();
});
});

const TRIGGER_TEXT = 'Trigger';
Expand Down Expand Up @@ -199,3 +218,33 @@ const UndefinedDescriptionSheetTest = (props: Sheet.Root.Props) => {
</Sheet.Root>
);
};

const RerenderSheetTest = (props: Sheet.Root.Props) => {
const [, rerender] = useState({});
return (
<Sheet.Root {...props}>
<Sheet.Trigger>{TRIGGER_TEXT}</Sheet.Trigger>
<Sheet.Popup>
<Sheet.Body>
<Tabs.Root defaultValue={'sort'}>
<Tabs.List>
<Tabs.Trigger value="sort">Sort</Tabs.Trigger>
<Tabs.Trigger value="package">Package</Tabs.Trigger>
<Tabs.Trigger value="status">Status</Tabs.Trigger>
<Tabs.Trigger value="tag">Tag</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Panel value="sort">1</Tabs.Panel>
<Tabs.Panel value="package" data-testid="package-panel">
<button data-testid="package-button" onClick={() => rerender({})}>
rerender
</button>
</Tabs.Panel>
<Tabs.Panel value="status">3</Tabs.Panel>
<Tabs.Panel value="tag">4</Tabs.Panel>
</Tabs.Root>
</Sheet.Body>
</Sheet.Popup>
</Sheet.Root>
);
};
8 changes: 4 additions & 4 deletions packages/core/src/components/sheet/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -190,9 +190,9 @@ SheetPopupPrimitive.displayName = 'Sheet.PopupPrimitive';

export const SheetPopup = forwardRef<HTMLDivElement, SheetPopup.Props>(
({ portalElement, overlayElement, positionerElement, ...props }, ref) => {
const PortalElement = createSlot(portalElement || <SheetPortalPrimitive />);
const OverlayElement = createSlot(overlayElement || <SheetOverlayPrimitive />);
const PositionerElement = createSlot(positionerElement || <SheetPositionerPrimitive />);
const PortalElement = useSlot(portalElement, <SheetPortalPrimitive />);
const OverlayElement = useSlot(overlayElement, <SheetOverlayPrimitive />);
const PositionerElement = useSlot(positionerElement, <SheetPositionerPrimitive />);

return (
<PortalElement>
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/switch/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -40,7 +40,7 @@ export const SwitchRoot = forwardRef<HTMLButtonElement, SwitchRoot.Props>((props

const dataAttrs = createDataAttributes({ invalid });

const ThumbElement = useMemo(() => createSlot(<SwitchThumbPrimitive />), []);
const ThumbElement = useSlot(useMemo(() => <SwitchThumbPrimitive />, []));
const children = childrenProp || <ThumbElement />;

return (
Expand Down
Loading
Loading