Skip to content

Commit 3c852b5

Browse files
committed
Fix more unstable references with useMemo
1 parent 3fe01eb commit 3c852b5

File tree

11 files changed

+72
-43
lines changed

11 files changed

+72
-43
lines changed

packages/@react-aria/interactions/src/useHover.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ export function useHover(props: HoverProps): HoverResult {
213213
// eslint-disable-next-line react-hooks/exhaustive-deps
214214
}, [isDisabled]);
215215

216-
return {
216+
return useMemo(() => ({
217217
hoverProps,
218218
isHovered
219-
};
219+
}), [hoverProps, isHovered]);
220220
}

packages/@react-aria/listbox/src/useOption.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {getItemId, listData} from './utils';
1717
import {isFocusVisible, useHover} from '@react-aria/interactions';
1818
import {ListState} from '@react-stately/list';
1919
import {SelectableItemStates, useSelectableItem} from '@react-aria/selection';
20+
import {useCallback} from 'react';
2021

2122
export interface OptionAria extends SelectableItemStates {
2223
/** Props for the option element. */
@@ -142,12 +143,12 @@ export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: R
142143

143144
let {hoverProps} = useHover({
144145
isDisabled: isDisabled || !shouldFocusOnHover,
145-
onHoverStart() {
146+
onHoverStart: useCallback(() => {
146147
if (!isFocusVisible()) {
147148
state.selectionManager.setFocused(true);
148149
state.selectionManager.setFocusedKey(key);
149150
}
150-
}
151+
}, [state.selectionManager])
151152
});
152153

153154
let domProps = filterDOMProps(item?.props);

packages/@react-aria/selection/src/useSelectableItem.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {getCollectionId, isNonContiguousSelectionModifier} from './utils';
1616
import {isCtrlKeyPressed, mergeProps, openLink, useId, useRouter} from '@react-aria/utils';
1717
import {moveVirtualFocus} from '@react-aria/focus';
1818
import {MultipleSelectionManager} from '@react-stately/selection';
19-
import {useEffect, useRef} from 'react';
19+
import {useEffect, useRef, useCallback} from 'react';
2020

2121
export interface SelectableItemOptions extends DOMProps {
2222
/**
@@ -184,14 +184,17 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
184184
// item is tabbable. If using virtual focus, don't set a tabIndex at all so that VoiceOver
185185
// on iOS 14 doesn't try to move real DOM focus to the item anyway.
186186
let itemProps: SelectableItemAria['itemProps'] = {};
187+
188+
const onFocus = useCallback(e => {
189+
if (e.target === ref.current) {
190+
manager.setFocusedKey(key);
191+
}
192+
}, [manager]);
193+
187194
if (!shouldUseVirtualFocus && !isDisabled) {
188195
itemProps = {
189196
tabIndex: key === manager.focusedKey ? 0 : -1,
190-
onFocus(e) {
191-
if (e.target === ref.current) {
192-
manager.setFocusedKey(key);
193-
}
194-
}
197+
onFocus
195198
};
196199
} else if (isDisabled) {
197200
itemProps.onMouseDown = (e) => {
@@ -365,11 +368,11 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
365368
// Once the user is in selection mode, they can long press again to drag.
366369
// Use a capturing listener to ensure this runs before useDrag, regardless of
367370
// the order the props get merged.
368-
let onDragStartCapture = e => {
371+
let onDragStartCapture = useCallback(e => {
369372
if (modality.current === 'touch' && longPressEnabledOnPressStart.current) {
370373
e.preventDefault();
371374
}
372-
};
375+
}, []);
373376

374377
// Prevent default on link clicks so that we control exactly
375378
// when they open (to match selection behavior).

packages/@react-aria/utils/src/useGlobalListeners.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {useCallback, useEffect, useRef} from 'react';
13+
import {useCallback, useEffect, useRef, useMemo} from 'react';
1414

1515
interface GlobalListeners {
1616
addGlobalListener<K extends keyof WindowEventMap>(el: Window, type: K, listener: (this: Document, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void,
@@ -48,5 +48,5 @@ export function useGlobalListeners(): GlobalListeners {
4848
return removeAllGlobalListeners;
4949
}, [removeAllGlobalListeners]);
5050

51-
return {addGlobalListener, removeGlobalListener, removeAllGlobalListeners};
51+
return useMemo(() => ({addGlobalListener, removeGlobalListener, removeAllGlobalListeners}), [addGlobalListener, removeGlobalListener, removeAllGlobalListeners]);
5252
}

packages/@react-stately/form/src/useFormValidationState.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function useFormValidationState<T>(props: FormValidationProps<T>): FormVa
6666
// Private prop for parent components to pass state to children.
6767
if (props[privateValidationStateProp]) {
6868
let {realtimeValidation, displayValidation, updateValidation, resetValidation, commitValidation} = props[privateValidationStateProp] as FormValidationState;
69-
return {realtimeValidation, displayValidation, updateValidation, resetValidation, commitValidation};
69+
return useMemo(() => ({realtimeValidation, displayValidation, updateValidation, resetValidation, commitValidation}), [realtimeValidation, displayValidation, updateValidation, resetValidation, commitValidation]);
7070
}
7171

7272
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -152,7 +152,7 @@ function useFormValidationStateImpl<T>(props: FormValidationProps<T>): FormValid
152152
? controlledError || serverError || currentValidity
153153
: controlledError || serverError || clientError || builtinValidation || currentValidity;
154154

155-
return {
155+
return useMemo(() => ({
156156
realtimeValidation,
157157
displayValidation,
158158
updateValidation(value) {
@@ -188,7 +188,7 @@ function useFormValidationStateImpl<T>(props: FormValidationProps<T>): FormValid
188188
}
189189
setServerErrorCleared(true);
190190
}
191-
};
191+
}), [realtimeValidation, displayValidation, validationBehavior, currentValidity]);
192192
}
193193

194194
function asArray<T>(v: T | T[]): T[] {

packages/@react-stately/list/src/useListState.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export interface ListState<T> {
3535
/** A set of items that are disabled. */
3636
disabledKeys: Set<Key>,
3737

38+
/** The focused key */
39+
focusedKey: Key | null,
40+
3841
/** A selection manager to read and update multiple selection state. */
3942
selectionManager: SelectionManager
4043
}
@@ -66,22 +69,24 @@ export function useListState<T extends object>(props: ListProps<T>): ListState<T
6669
return useMemo(() => ({
6770
collection,
6871
disabledKeys,
72+
focusedKey: selectionState.focusedKey,
6973
selectionManager
70-
}), [collection, disabledKeys, selectionManager]);
74+
}), [collection, disabledKeys, selectionState.focusedKey, selectionManager]);
7175
}
7276

7377
/**
7478
* Filters a collection using the provided filter function and returns a new ListState.
7579
*/
7680
export function UNSTABLE_useFilteredListState<T extends object>(state: ListState<T>, filter: ((nodeValue: string) => boolean) | null | undefined): ListState<T> {
7781
let collection = useMemo(() => filter ? state.collection.UNSTABLE_filter!(filter) : state.collection, [state.collection, filter]);
78-
let selectionManager = useMemo(() => state.selectionManager.withCollection(collection), [collection]);
82+
let selectionManager = useMemo(() => state.selectionManager.withCollection(collection), [state, collection]);
7983
useFocusedKeyReset(collection, selectionManager);
8084
return useMemo(() => ({
8185
collection,
8286
selectionManager,
87+
focusedKey: selectionManager.focusedKey,
8388
disabledKeys: state.disabledKeys
84-
}), [collection, selectionManager, state.disabledKeys]);
89+
}), [collection, selectionManager, selectionManager.focusedKey, state.disabledKeys]);
8590
}
8691

8792
function useFocusedKeyReset<T>(collection: Collection<Node<T>>, selectionManager: SelectionManager) {

packages/@react-stately/list/src/useSingleSelectListState.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {CollectionStateBase, Key, Node, Selection, SingleSelection} from '@react-types/shared';
1414
import {ListState, useListState} from './useListState';
1515
import {useControlledState} from '@react-stately/utils';
16-
import {useMemo} from 'react';
16+
import {useMemo, useCallback} from 'react';
1717

1818
export interface SingleSelectListProps<T> extends CollectionStateBase<T>, Omit<SingleSelection, 'disallowEmptySelection'> {
1919
/** Filter function to generate a filtered list of nodes. */
@@ -46,7 +46,7 @@ export function useSingleSelectListState<T extends object>(props: SingleSelectLi
4646
disallowEmptySelection: true,
4747
allowDuplicateSelectionEvents: true,
4848
selectedKeys,
49-
onSelectionChange: (keys: Selection) => {
49+
onSelectionChange: useCallback((keys: Selection) => {
5050
// impossible, but TS doesn't know that
5151
if (keys === 'all') {
5252
return;
@@ -60,19 +60,20 @@ export function useSingleSelectListState<T extends object>(props: SingleSelectLi
6060
}
6161

6262
setSelectedKey(key);
63-
}
63+
}, [props.onSelectionChange, selectedKey, setSelectedKey])
6464
});
6565

6666
let selectedItem = selectedKey != null
6767
? collection.getItem(selectedKey)
6868
: null;
6969

70-
return {
70+
return useMemo(() => ({
7171
collection,
7272
disabledKeys,
7373
selectionManager,
74+
focusedKey: selectionManager.focusedKey,
7475
selectedKey,
7576
setSelectedKey,
7677
selectedItem
77-
};
78+
}), [collection, disabledKeys, selectionManager, selectedKey, setSelectedKey, selectedItem]);
7879
}

packages/@react-stately/overlays/src/useOverlayTriggerState.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {OverlayTriggerProps} from '@react-types/overlays';
14-
import {useCallback} from 'react';
14+
import {useCallback, useMemo} from 'react';
1515
import {useControlledState} from '@react-stately/utils';
1616

1717
export interface OverlayTriggerState {
@@ -46,11 +46,11 @@ export function useOverlayTriggerState(props: OverlayTriggerProps): OverlayTrigg
4646
setOpen(!isOpen);
4747
}, [setOpen, isOpen]);
4848

49-
return {
49+
return useMemo(() => ({
5050
isOpen,
5151
setOpen,
5252
open,
5353
close,
5454
toggle
55-
};
55+
}), [isOpen, setOpen, open, close, toggle]);
5656
}

packages/@react-stately/select/src/useSelectState.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {FormValidationState, useFormValidationState} from '@react-stately/form';
1515
import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays';
1616
import {SelectProps} from '@react-types/select';
1717
import {SingleSelectListState, useSingleSelectListState} from '@react-stately/list';
18-
import {useState} from 'react';
18+
import {useState, useMemo, useEffect, useCallback, useRef} from 'react';
1919

2020
export interface SelectStateOptions<T> extends Omit<SelectProps<T>, 'children'>, CollectionStateBase<T> {}
2121

@@ -44,26 +44,34 @@ export interface SelectState<T> extends SingleSelectListState<T>, OverlayTrigger
4444
export function useSelectState<T extends object>(props: SelectStateOptions<T>): SelectState<T> {
4545
let triggerState = useOverlayTriggerState(props);
4646
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy | null>(null);
47+
48+
let validationStateRef = useRef<FormValidationState>(null);
49+
4750
let listState = useSingleSelectListState({
4851
...props,
49-
onSelectionChange: (key) => {
52+
onSelectionChange: useCallback(key => {
5053
if (props.onSelectionChange != null) {
5154
props.onSelectionChange(key);
5255
}
5356

5457
triggerState.close();
55-
validationState.commitValidation();
56-
}
58+
validationStateRef.current!.commitValidation();
59+
}, [props.onSelectionChange, triggerState])
5760
});
5861

5962
let validationState = useFormValidationState({
6063
...props,
6164
value: listState.selectedKey
6265
});
66+
validationStateRef.current = validationState;
6367

6468
let [isFocused, setFocused] = useState(false);
6569

66-
return {
70+
useEffect(() => {
71+
console.log("come on");
72+
}, [triggerState])
73+
74+
return useMemo(() => ({
6775
...validationState,
6876
...listState,
6977
...triggerState,
@@ -83,5 +91,5 @@ export function useSelectState<T extends object>(props: SelectStateOptions<T>):
8391
},
8492
isFocused,
8593
setFocused
86-
};
94+
}), [validationState, listState, triggerState, focusStrategy, isFocused]);
8795
}

packages/@react-stately/selection/src/useMultipleSelectionState.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function useMultipleSelectionState(props: MultipleSelectionStateProps): M
8585
}
8686
}, [selectionBehaviorProp]);
8787

88-
return {
88+
return useMemo(() => ({
8989
selectionMode,
9090
disallowEmptySelection,
9191
selectionBehavior,
@@ -116,7 +116,7 @@ export function useMultipleSelectionState(props: MultipleSelectionStateProps): M
116116
},
117117
disabledKeys: disabledKeysProp,
118118
disabledBehavior
119-
};
119+
}), [selectionMode, disallowEmptySelection, selectionBehavior, selectedKeys, setSelectedKeys, allowDuplicateSelectionEvents, disabledKeysProp, disabledBehavior]);
120120
}
121121

122122
function convertSelection(selection: 'all' | Iterable<Key> | null | undefined, defaultValue?: Selection): 'all' | Set<Key> | undefined {

packages/react-aria-components/src/ListBox.tsx

+18-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria';
13+
import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption, OptionAria} from 'react-aria';
1414
import {Collection, CollectionBuilder, createBranchComponent, createLeafComponent} from '@react-aria/collections';
1515
import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps} from './Collection';
1616
import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
@@ -20,10 +20,11 @@ import {DraggableCollectionState, DroppableCollectionState, ListState, Node, Ori
2020
import {filterDOMProps, mergeRefs, useObjectRef} from '@react-aria/utils';
2121
import {forwardRefType, HoverEvents, Key, LinkDOMProps, RefObject} from '@react-types/shared';
2222
import {HeaderContext} from './Header';
23-
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
23+
import React, {createContext, ForwardedRef, forwardRef, JSX, memo, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
2424
import {SeparatorContext} from './Separator';
2525
import {TextContext} from './Text';
2626
import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete';
27+
import {SelectionManager} from '@react-stately/selection';
2728

2829
export interface ListBoxRenderProps {
2930
/**
@@ -332,13 +333,23 @@ export interface ListBoxItemProps<T = object> extends RenderProps<ListBoxItemRen
332333
export const ListBoxItem = /*#__PURE__*/ createLeafComponent('item', function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, forwardedRef: ForwardedRef<HTMLDivElement>, item: Node<T>) {
333334
let ref = useObjectRef<any>(forwardedRef);
334335
let state = useContext(ListStateContext)!;
335-
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext)!;
336-
let {optionProps, labelProps, descriptionProps, ...states} = useOption(
336+
337+
let options = useOption(
337338
{key: item.key, 'aria-label': props?.['aria-label']},
338339
state,
339340
ref
340341
);
341342

343+
return <ListBoxItemInner selectionManager={state.selectionManager} options={options} focused={item.key === state.focusedKey} passRef={ref} props={props} item={item} />
344+
});
345+
346+
const ListBoxItemInner = memo(function ListBoxItemInner<T extends object>({props, item, selectionManager, options, focused, passRef}:
347+
{options: OptionAria, props: ListBoxItemProps<T>, focused: boolean, selectionManager: SelectionManager, item: Node<T>, passRef: React.MutableRefObject<any>}) {
348+
const ref = passRef;
349+
350+
let {dragAndDropHooks, dragState, dropState} = useContext(DragAndDropContext)!;
351+
let {optionProps, labelProps, descriptionProps, ...states} = options;
352+
342353
let {hoverProps, isHovered} = useHover({
343354
isDisabled: !states.allowsSelection && !states.hasAction,
344355
onHoverStart: item.props.onHoverStart,
@@ -367,8 +378,8 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent('item', function Li
367378
values: {
368379
...states,
369380
isHovered,
370-
selectionMode: state.selectionManager.selectionMode,
371-
selectionBehavior: state.selectionManager.selectionBehavior,
381+
selectionMode: selectionManager.selectionMode,
382+
selectionBehavior: selectionManager.selectionBehavior,
372383
allowsDragging: !!dragState,
373384
isDragging,
374385
isDropTarget: droppableItem?.isDropTarget
@@ -397,7 +408,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent('item', function Li
397408
data-pressed={states.isPressed || undefined}
398409
data-dragging={isDragging || undefined}
399410
data-drop-target={droppableItem?.isDropTarget || undefined}
400-
data-selection-mode={state.selectionManager.selectionMode === 'none' ? undefined : state.selectionManager.selectionMode}>
411+
data-selection-mode={selectionManager.selectionMode === 'none' ? undefined : selectionManager.selectionMode}>
401412
<Provider
402413
values={[
403414
[TextContext, {

0 commit comments

Comments
 (0)