Skip to content
Merged
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This repository contains the source code and documentation for Base UI: a headl
## Code guidelines

- Always use the `useTimeout` utility from `@base-ui-components/utils/useTimeout` instead of `window.setTimeout`, and `useAnimationFrame` from `@base-ui-components/utils/useAnimationFrame` instead of `requestAnimationFrame`. Search for other example usage in the codebase if unsure how to use them.
- Use the `useEventCallback` utility from `@base-ui-components/utils/useEventCallback` instead of `React.useCallback` if the function is called within an effect or event handler. The utility cannot be used to memoize functions that are called directly in the body of a component (during render), so continue with `React.useCallback` in those scenarios.
- Use the `useStableCallback` utility from `@base-ui-components/utils/useStableCallback` instead of `React.useCallback` if the function is called within an effect or event handler. The utility cannot be used to memoize functions that are called directly in the body of a component (during render), so continue with `React.useCallback` in those scenarios.
- Always use the `useIsoLayoutEffect` utility from `@base-ui-components/utils/useIsoLayoutEffect` instead of `React.useLayoutEffect`.
- Avoid duplicating logic where necessary. If two components can share logic (such as event handlers), define the logic/handlers in the parent and share it through a context to the child; use the existing context if it exists.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit';
import { ReactStore } from '@base-ui-components/utils/store';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';

export default function Playground() {
const [open, setOpen] = React.useState(false);
Expand Down Expand Up @@ -81,7 +81,7 @@ function ControllableComponent(props: Props) {
const open = store.useState('open');
const value = store.useState('value');

const handleClick = useEventCallback(() => {
const handleClick = useStableCallback(() => {
store.set('open', !open);
props.onOpenChange?.(!open, 'toggle-button');
});
Expand Down
4 changes: 2 additions & 2 deletions docs/src/components/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import clsx from 'clsx';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { observeScrollableInner } from '../utils/observeScrollableInner';

const ARROW_UP = 'ArrowUp';
Expand Down Expand Up @@ -107,7 +107,7 @@ export function Item(props: React.ComponentProps<'details'>) {
// in Chrome, the <details> opens automatically when the hash part of a URL
// matches the `id` on <summary> but needs to be manually handled for Safari
// and Firefox
const handleRef = useEventCallback((element: HTMLDetailsElement | null) => {
const handleRef = useStableCallback((element: HTMLDetailsElement | null) => {
if (element) {
const trigger = element.querySelector<HTMLElement>('summary');
const triggerId = trigger?.getAttribute('id');
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/accordion/item/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import * as React from 'react';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs';
import { BaseUIComponentProps } from '../../utils/types';
import { useBaseUiId } from '../../utils/useBaseUiId';
Expand Down Expand Up @@ -64,7 +64,7 @@ export const AccordionItem = React.forwardRef(function AccordionItem(
return false;
}, [openValues, value]);

const onOpenChange = useEventCallback(
const onOpenChange = useStableCallback(
(nextOpen: boolean, eventDetails: CollapsibleRoot.ChangeEventDetails) => {
onOpenChangeProp?.(nextOpen, eventDetails);

Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/accordion/root/AccordionRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useControlled } from '@base-ui-components/utils/useControlled';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { warn } from '@base-ui-components/utils/warn';
import { BaseUIComponentProps, Orientation } from '../../utils/types';
Expand Down Expand Up @@ -66,7 +66,7 @@ export const AccordionRoot = React.forwardRef(function AccordionRoot(
return undefined;
}, [valueProp, defaultValueProp]);

const onValueChange = useEventCallback(onValueChangeProp);
const onValueChange = useStableCallback(onValueChangeProp);

const accordionItemRefs = React.useRef<(HTMLElement | null)[]>([]);

Expand All @@ -77,7 +77,7 @@ export const AccordionRoot = React.forwardRef(function AccordionRoot(
state: 'value',
});

const handleValueChange = useEventCallback((newValue: number | string, nextOpen: boolean) => {
const handleValueChange = useStableCallback((newValue: number | string, nextOpen: boolean) => {
const details = createChangeEventDetails('none');
if (!multiple) {
const nextValue = value[0] === newValue ? [] : [newValue];
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/autocomplete/root/AutocompleteRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import * as React from 'react';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { AriaCombobox } from '../../combobox/root/AriaCombobox';
import { useCoreFilter } from '../../combobox/root/utils/useFilter';
import { stringifyAsLabel } from '../../utils/resolveValueLabel';
Expand Down Expand Up @@ -67,7 +67,7 @@ export function AutocompleteRoot<ItemValue>(
resolvedInputValue = internalValue;
}

const handleValueChange = useEventCallback(
const handleValueChange = useStableCallback(
(nextValue: string, eventDetails: AutocompleteRoot.ChangeEventDetails) => {
setInlineInputValue('');
if (!isControlled) {
Expand Down Expand Up @@ -100,7 +100,7 @@ export function AutocompleteRoot<ItemValue>(
};
}, [baseFilter, mode, other.filter, resolvedQuery, staticItems]);

const handleItemHighlighted = useEventCallback(
const handleItemHighlighted = useStableCallback(
(highlightedValue: any, eventDetails: AriaCombobox.HighlightEventDetails) => {
props.onItemHighlighted?.(highlightedValue, eventDetails);

Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/avatar/image/AvatarImage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import * as React from 'react';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { BaseUIComponentProps } from '../../utils/types';
import { useRenderElement } from '../../utils/useRenderElement';
Expand Down Expand Up @@ -34,7 +34,7 @@ export const AvatarImage = React.forwardRef(function AvatarImage(
crossOrigin,
});

const handleLoadingStatusChange = useEventCallback((status: ImageLoadingStatus) => {
const handleLoadingStatusChange = useStableCallback((status: ImageLoadingStatus) => {
onLoadingStatusChangeProp?.(status);
context.setImageLoadingStatus(status);
});
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/checkbox-group/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useControlled } from '@base-ui-components/utils/useControlled';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useBaseUiId } from '../utils/useBaseUiId';
import { useRenderElement } from '../utils/useRenderElement';
import { CheckboxGroupContext } from './CheckboxGroupContext';
Expand Down Expand Up @@ -51,7 +51,7 @@ export const CheckboxGroup = React.forwardRef(function CheckboxGroup(
state: 'value',
});

const setValue = useEventCallback(
const setValue = useStableCallback(
(v: string[], eventDetails: CheckboxGroup.ChangeEventDetails) => {
onValueChange?.(v, eventDetails);

Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/checkbox-group/useCheckboxGroupParent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import * as React from 'react';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useBaseUiId } from '../utils/useBaseUiId';
import type { BaseUIChangeEventDetails } from '../utils/createBaseUIEventDetails';

Expand All @@ -20,7 +20,7 @@ export function useCheckboxGroupParent(
const checked = value.length === allValues.length;
const indeterminate = value.length !== allValues.length && value.length > 0;

const onValueChange = useEventCallback(onValueChangeProp);
const onValueChange = useStableCallback(onValueChangeProp);

const getParentProps: useCheckboxGroupParent.ReturnValue['getParentProps'] = React.useCallback(
() => ({
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/checkbox/root/CheckboxRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import { EMPTY_OBJECT } from '@base-ui-components/utils/empty';
import { useControlled } from '@base-ui-components/utils/useControlled';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs';
import { visuallyHidden } from '@base-ui-components/utils/visuallyHidden';
Expand Down Expand Up @@ -96,7 +96,7 @@ export const CheckboxRoot = React.forwardRef(function CheckboxRoot(
}
}

const onCheckedChange = useEventCallback(onCheckedChangeProp);
const onCheckedChange = useStableCallback(onCheckedChangeProp);

const {
checked: groupChecked = checkedProp,
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/collapsible/panel/useCollapsiblePanel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useMergedRefs } from '@base-ui-components/utils/useMergedRefs';
import { useOnMount } from '@base-ui-components/utils/useOnMount';
import { AnimationFrame } from '@base-ui-components/utils/useAnimationFrame';
Expand Down Expand Up @@ -63,7 +63,7 @@ export function useCollapsiblePanel(
* time it opens. If the panel is in the middle of a close transition that is
* interrupted and re-opens, this won't run as the panel was not unmounted.
*/
const handlePanelRef = useEventCallback((element: HTMLElement) => {
const handlePanelRef = useStableCallback((element: HTMLElement) => {
if (!element) {
return undefined;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/collapsible/root/CollapsibleRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import * as React from 'react';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { BaseUIComponentProps } from '../../utils/types';
import { useRenderElement } from '../../utils/useRenderElement';
import { useCollapsibleRoot } from './useCollapsibleRoot';
Expand Down Expand Up @@ -28,7 +28,7 @@ export const CollapsibleRoot = React.forwardRef(function CollapsibleRoot(
...elementProps
} = componentProps;

const onOpenChange = useEventCallback(onOpenChangeProp);
const onOpenChange = useStableCallback(onOpenChangeProp);

const collapsible = useCollapsibleRoot({
open,
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/collapsible/root/useCollapsibleRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import { useControlled } from '@base-ui-components/utils/useControlled';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useBaseUiId } from '../../utils/useBaseUiId';
import { createChangeEventDetails } from '../../utils/createBaseUIEventDetails';
import { useAnimationsFinished } from '../../utils/useAnimationsFinished';
Expand Down Expand Up @@ -51,7 +51,7 @@ export function useCollapsibleRoot(

const runOnceAnimationsFinish = useAnimationsFinished(panelRef, false);

const handleTrigger = useEventCallback((event: React.MouseEvent | React.KeyboardEvent) => {
const handleTrigger = useStableCallback((event: React.MouseEvent | React.KeyboardEvent) => {
const nextOpen = !open;
const eventDetails = createChangeEventDetails('trigger-press', event.nativeEvent);

Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/combobox/input/ComboboxInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { useStore } from '@base-ui-components/utils/store';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { isAndroid, isFirefox } from '@base-ui-components/utils/detectBrowser';
import { BaseUIComponentProps } from '../../utils/types';
import { useBaseUiId } from '../../utils/useBaseUiId';
Expand Down Expand Up @@ -79,7 +79,7 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
const [composingValue, setComposingValue] = React.useState<string | null>(null);
const isComposingRef = React.useRef(false);

const setInputElement = useEventCallback((element) => {
const setInputElement = useStableCallback((element) => {
// The search filter for the input-inside-popup pattern should be empty initially.
if (hasPositionerParent && !store.state.hasInputValue) {
store.state.setInputValue('', createChangeEventDetails('none'));
Expand Down
5 changes: 1 addition & 4 deletions packages/react/src/combobox/item/ComboboxItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';
import * as React from 'react';
import { useStore } from '@base-ui-components/utils/store';
import { useLatestRef } from '@base-ui-components/utils/useLatestRef';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import {
useComboboxRootContext,
Expand Down Expand Up @@ -70,7 +69,6 @@ export const ComboboxItem = React.memo(
const getItemProps = useStore(store, selectors.getItemProps);

const itemRef = React.useRef<HTMLDivElement | null>(null);
const indexRef = useLatestRef(index);

const hasRegistered = listItem.index !== -1;

Expand Down Expand Up @@ -175,10 +173,9 @@ export const ComboboxItem = React.memo(
const contextValue: ComboboxItemContext = React.useMemo(
() => ({
selected,
indexRef,
textRef,
}),
[selected, indexRef, textRef],
[selected, textRef],
);

return (
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/combobox/item/ComboboxItemContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';

export interface ComboboxItemContext {
selected: boolean;
indexRef: React.RefObject<number>;
textRef: React.RefObject<HTMLElement | null>;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/combobox/list/ComboboxList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useStore } from '@base-ui-components/utils/store';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import type { BaseUIComponentProps } from '../../utils/types';
import { useRenderElement } from '../../utils/useRenderElement';
Expand Down Expand Up @@ -45,11 +45,11 @@ export const ComboboxList = React.forwardRef(function ComboboxList(
const multiple = selectionMode === 'multiple';
const empty = filteredItems.length === 0;

const setPositionerElement = useEventCallback((element) => {
const setPositionerElement = useStableCallback((element) => {
store.set('positionerElement', element);
});

const setListElement = useEventCallback((element) => {
const setListElement = useStableCallback((element) => {
store.set('listElement', element);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useStore } from '@base-ui-components/utils/store';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { inertValue } from '@base-ui-components/utils/inertValue';
import {
useComboboxFloatingContext,
Expand Down Expand Up @@ -135,7 +135,7 @@ export const ComboboxPositioner = React.forwardRef(function ComboboxPositioner(
],
);

const setPositionerElement = useEventCallback((element) => {
const setPositionerElement = useStableCallback((element) => {
store.set('positionerElement', element);
});

Expand Down
Loading
Loading