Skip to content

Commit 7dd7b48

Browse files
authored
Refactor collection internals in preparation for public API (#6608)
1 parent f8d63a6 commit 7dd7b48

25 files changed

+361
-304
lines changed

packages/@react-stately/grid/src/useGridState.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {getChildNodes, getFirstItem, getLastItem} from '@react-stately/collections';
22
import {GridCollection, GridNode} from '@react-types/grid';
33
import {Key} from '@react-types/shared';
4-
import {MultipleSelectionStateProps, SelectionManager, useMultipleSelectionState} from '@react-stately/selection';
4+
import {MultipleSelectionState, MultipleSelectionStateProps, SelectionManager, useMultipleSelectionState} from '@react-stately/selection';
55
import {useEffect, useMemo, useRef} from 'react';
66

77
export interface GridState<T, C extends GridCollection<T>> {
@@ -17,15 +17,18 @@ export interface GridState<T, C extends GridCollection<T>> {
1717
export interface GridStateOptions<T, C extends GridCollection<T>> extends MultipleSelectionStateProps {
1818
collection: C,
1919
disabledKeys?: Iterable<Key>,
20-
focusMode?: 'row' | 'cell'
20+
focusMode?: 'row' | 'cell',
21+
/** @private - do not use unless you know what you're doing. */
22+
UNSAFE_selectionState?: MultipleSelectionState
2123
}
2224

2325
/**
2426
* Provides state management for a grid component. Handles row selection and focusing a grid cell's focusable child if applicable.
2527
*/
2628
export function useGridState<T extends object, C extends GridCollection<T>>(props: GridStateOptions<T, C>): GridState<T, C> {
2729
let {collection, focusMode} = props;
28-
let selectionState = useMultipleSelectionState(props);
30+
// eslint-disable-next-line react-hooks/rules-of-hooks
31+
let selectionState = props.UNSAFE_selectionState || useMultipleSelectionState(props);
2932
let disabledKeys = useMemo(() =>
3033
props.disabledKeys ? new Set(props.disabledKeys) : new Set<Key>()
3134
, [props.disabledKeys]);

packages/@react-stately/table/src/useTableState.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {GridState, useGridState} from '@react-stately/grid';
1414
import {TableCollection as ITableCollection, TableBodyProps, TableHeaderProps} from '@react-types/table';
1515
import {Key, Node, SelectionMode, Sortable, SortDescriptor, SortDirection} from '@react-types/shared';
16-
import {MultipleSelectionStateProps} from '@react-stately/selection';
16+
import {MultipleSelectionState, MultipleSelectionStateProps} from '@react-stately/selection';
1717
import {ReactElement, useCallback, useMemo, useState} from 'react';
1818
import {TableCollection} from './TableCollection';
1919
import {useCollection} from '@react-stately/collections';
@@ -52,7 +52,9 @@ export interface TableStateProps<T> extends MultipleSelectionStateProps, Sortabl
5252
/** Whether the row drag button should be displayed.
5353
* @private
5454
*/
55-
showDragButtons?: boolean
55+
showDragButtons?: boolean,
56+
/** @private - do not use unless you know what you're doing. */
57+
UNSAFE_selectionState?: MultipleSelectionState
5658
}
5759

5860
const OPPOSITE_SORT_DIRECTION = {

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

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import {AriaBreadcrumbsProps} from 'react-aria';
13-
import {Collection, Node} from 'react-stately';
14-
import {CollectionProps, CollectionRendererContext, createLeafComponent, useCollection} from './Collection';
13+
import {Collection, CollectionBuilder, CollectionProps, CollectionRendererContext, createLeafComponent} from './Collection';
1514
import {ContextValue, forwardRefType, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlottedContext} from './utils';
1615
import {filterDOMProps} from '@react-aria/utils';
1716
import {Key} from '@react-types/shared';
1817
import {LinkContext} from './Link';
19-
import React, {createContext, ForwardedRef, forwardRef, ReactNode, RefObject, useContext} from 'react';
18+
import {Node} from 'react-stately';
19+
import React, {createContext, ForwardedRef, forwardRef, ReactNode, useContext} from 'react';
2020

2121
export interface BreadcrumbsProps<T> extends Omit<CollectionProps<T>, 'disabledKeys'>, AriaBreadcrumbsProps, StyleProps, SlotProps {
2222
/** Whether the breadcrumbs are disabled. */
@@ -29,36 +29,23 @@ export const BreadcrumbsContext = createContext<ContextValue<BreadcrumbsProps<an
2929

3030
function Breadcrumbs<T extends object>(props: BreadcrumbsProps<T>, ref: ForwardedRef<HTMLOListElement>) {
3131
[props, ref] = useContextProps(props, ref, BreadcrumbsContext);
32-
let {portal, collection} = useCollection(props);
33-
34-
// Render the portal first so that we have the collection by the time we render the DOM in SSR
35-
return (
36-
<>
37-
{portal}
38-
<BreadcrumbsInner props={props} collection={collection} breadcrumbsRef={ref} />
39-
</>
40-
);
41-
}
42-
43-
interface BreadcrumbsInnerProps<T> {
44-
props: BreadcrumbsProps<T>,
45-
collection: Collection<Node<T>>,
46-
breadcrumbsRef: RefObject<HTMLOListElement | null>
47-
}
48-
49-
function BreadcrumbsInner<T extends object>({props, collection, breadcrumbsRef: ref}: BreadcrumbsInnerProps<T>) {
5032
let {CollectionRoot} = useContext(CollectionRendererContext);
33+
5134
return (
52-
<ol
53-
ref={ref}
54-
{...filterDOMProps(props, {labelable: true})}
55-
slot={props.slot || undefined}
56-
style={props.style}
57-
className={props.className ?? 'react-aria-Breadcrumbs'}>
58-
<BreadcrumbsContext.Provider value={props}>
59-
<CollectionRoot collection={collection} />
60-
</BreadcrumbsContext.Provider>
61-
</ol>
35+
<CollectionBuilder content={<Collection {...props} />}>
36+
{collection => (
37+
<ol
38+
ref={ref}
39+
{...filterDOMProps(props, {labelable: true})}
40+
slot={props.slot || undefined}
41+
style={props.style}
42+
className={props.className ?? 'react-aria-Breadcrumbs'}>
43+
<BreadcrumbsContext.Provider value={props}>
44+
<CollectionRoot collection={collection} />
45+
</BreadcrumbsContext.Provider>
46+
</ol>
47+
)}
48+
</CollectionBuilder>
6249
);
6350
}
6451

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

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
*/
1212
import {CollectionBase, DropTargetDelegate, Key, LayoutDelegate} from '@react-types/shared';
1313
import {createPortal} from 'react-dom';
14-
import {forwardRefType, StyleProps} from './utils';
14+
import {forwardRefType, Hidden, StyleProps} from './utils';
1515
import {Collection as ICollection, Node, SelectionBehavior, SelectionMode, SectionProps as SharedSectionProps} from 'react-stately';
1616
import {mergeProps, useIsSSR} from 'react-aria';
17-
import React, {cloneElement, createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactElement, ReactNode, RefObject, useCallback, useContext, useMemo, useRef} from 'react';
17+
import React, {cloneElement, createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactElement, ReactNode, RefObject, useCallback, useContext, useMemo, useRef, useState} from 'react';
1818
import {useLayoutEffect} from '@react-aria/utils';
1919
import {useSyncExternalStore as useSyncExternalStoreShim} from 'use-sync-external-store/shim/index.js';
2020

@@ -274,7 +274,7 @@ class BaseNode<T> {
274274
* A mutable element node in the fake DOM tree. It owns an immutable
275275
* Collection Node which is copied on write.
276276
*/
277-
export class ElementNode<T> extends BaseNode<T> {
277+
class ElementNode<T> extends BaseNode<T> {
278278
nodeType = 8; // COMMENT_NODE (we'd use ELEMENT_NODE but React DevTools will fail to get its dimensions)
279279
node: NodeValue<T>;
280280
private _index: number = 0;
@@ -503,7 +503,7 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
503503
* A mutable Document in the fake DOM. It owns an immutable Collection instance,
504504
* which is lazily copied on write during updates.
505505
*/
506-
export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extends BaseNode<T> {
506+
class Document<T, C extends BaseCollection<T> = BaseCollection<T>> extends BaseNode<T> {
507507
nodeType = 11; // DOCUMENT_FRAGMENT_NODE
508508
ownerDocument = this;
509509
dirtyNodes: Set<BaseNode<T>> = new Set();
@@ -709,16 +709,41 @@ export function useCollectionChildren<T extends object>(props: CachedChildrenOpt
709709
}
710710

711711
const ShallowRenderContext = createContext(false);
712+
const CollectionDocumentContext = createContext<Document<any, BaseCollection<any>> | null>(null);
712713

713-
interface CollectionResult<C> {
714-
portal: ReactNode,
715-
collection: C
714+
export interface CollectionBuilderProps<C extends BaseCollection<object>> {
715+
content: ReactNode,
716+
children: (collection: C) => ReactNode,
717+
createCollection?: () => C
716718
}
717719

718-
export function useCollection<T extends object, C extends BaseCollection<T>>(props: CollectionProps<T>, initialCollection?: C): CollectionResult<C> {
719-
let {collection, document} = useCollectionDocument<T, C>(initialCollection);
720-
let portal = useCollectionPortal<T, C>(props, document);
721-
return {portal, collection};
720+
export function CollectionBuilder<C extends BaseCollection<object>>(props: CollectionBuilderProps<C>) {
721+
// If a document was provided above us, we're already in a hidden tree. Just render the content.
722+
let doc = useContext(CollectionDocumentContext);
723+
if (doc) {
724+
return props.content;
725+
}
726+
727+
// Otherwise, render a hidden copy of the children so that we can build the collection before constructing the state.
728+
// This should always come before the real DOM content so we have built the collection by the time it renders during SSR.
729+
730+
// This is fine. CollectionDocumentContext never changes after mounting.
731+
// eslint-disable-next-line react-hooks/rules-of-hooks
732+
let {collection, document} = useCollectionDocument(props.createCollection);
733+
return (
734+
<>
735+
<Hidden>
736+
<CollectionDocumentContext.Provider value={document}>
737+
{props.content}
738+
</CollectionDocumentContext.Provider>
739+
</Hidden>
740+
<CollectionInner render={props.children} collection={collection} />
741+
</>
742+
);
743+
}
744+
745+
function CollectionInner({collection, render}) {
746+
return render(collection);
722747
}
723748

724749
interface CollectionDocumentResult<T, C extends BaseCollection<T>> {
@@ -747,10 +772,10 @@ const useSyncExternalStore = typeof React['useSyncExternalStore'] === 'function'
747772
? React['useSyncExternalStore']
748773
: useSyncExternalStoreFallback;
749774

750-
export function useCollectionDocument<T extends object, C extends BaseCollection<T>>(initialCollection?: C): CollectionDocumentResult<T, C> {
775+
function useCollectionDocument<T extends object, C extends BaseCollection<T>>(createCollection?: () => C): CollectionDocumentResult<T, C> {
751776
// The document instance is mutable, and should never change between renders.
752777
// useSyncExternalStore is used to subscribe to updates, which vends immutable Collection objects.
753-
let document = useMemo(() => new Document<T, C>(initialCollection || new BaseCollection() as C), [initialCollection]);
778+
let [document] = useState(() => new Document<T, C>(createCollection?.() || new BaseCollection() as C));
754779
let subscribe = useCallback((fn: () => void) => document.subscribe(fn), [document]);
755780
let getSnapshot = useCallback(() => {
756781
let collection = document.getCollection();
@@ -779,27 +804,6 @@ export function useCollectionDocument<T extends object, C extends BaseCollection
779804
}
780805

781806
const SSRContext = createContext<BaseNode<any> | null>(null);
782-
export const CollectionDocumentContext = createContext<Document<any, BaseCollection<any>> | null>(null);
783-
784-
export function useCollectionPortal<T extends object, C extends BaseCollection<T>>(props: CollectionProps<T>, document?: Document<T, C>): ReactNode {
785-
let ctx = useContext(CollectionDocumentContext);
786-
let doc = document ?? ctx!;
787-
let children = useCollectionChildren(props);
788-
let wrappedChildren = useMemo(() => (
789-
<ShallowRenderContext.Provider value>
790-
{children}
791-
</ShallowRenderContext.Provider>
792-
), [children]);
793-
// During SSR, we render the content directly, and append nodes to the document during render.
794-
// The collection children return null so that nothing is actually rendered into the HTML.
795-
return useIsSSR()
796-
? <SSRContext.Provider value={doc}>{wrappedChildren}</SSRContext.Provider>
797-
: createPortal(wrappedChildren, doc as unknown as Element);
798-
}
799-
800-
export function CollectionPortal<T extends object>(props: CollectionProps<T>) {
801-
return <>{useCollectionPortal(props)}</>;
802-
}
803807

804808
export interface ItemRenderProps {
805809
/**
@@ -919,7 +923,30 @@ export function Collection<T extends object>(props: CollectionProps<T>): JSX.Ele
919923
let ctx = useContext(CollectionContext)!;
920924
props = mergeProps(ctx, props);
921925
props.dependencies = (ctx?.dependencies || []).concat(props.dependencies);
922-
return <>{useCollectionChildren(props)}</>;
926+
let children = useCollectionChildren(props);
927+
928+
let doc = useContext(CollectionDocumentContext);
929+
if (doc) {
930+
return <CollectionRoot>{children}</CollectionRoot>;
931+
}
932+
933+
return <>{children}</>;
934+
}
935+
936+
function CollectionRoot({children}) {
937+
let doc = useContext(CollectionDocumentContext);
938+
let wrappedChildren = useMemo(() => (
939+
<CollectionDocumentContext.Provider value={null}>
940+
<ShallowRenderContext.Provider value>
941+
{children}
942+
</ShallowRenderContext.Provider>
943+
</CollectionDocumentContext.Provider>
944+
), [children]);
945+
// During SSR, we render the content directly, and append nodes to the document during render.
946+
// The collection children return null so that nothing is actually rendered into the HTML.
947+
return useIsSSR()
948+
? <SSRContext.Provider value={doc}>{wrappedChildren}</SSRContext.Provider>
949+
: createPortal(wrappedChildren, doc as unknown as Element);
923950
}
924951

925952
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>) => JSX.Element): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
@@ -976,7 +1003,7 @@ export interface CollectionRenderer {
9761003
CollectionBranch: React.ComponentType<CollectionBranchProps>
9771004
}
9781005

979-
const DefaultCollectionRenderer: CollectionRenderer = {
1006+
export const DefaultCollectionRenderer: CollectionRenderer = {
9801007
CollectionRoot({collection}) {
9811008
return useCachedChildren({
9821009
items: collection,

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

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
import {AriaComboBoxProps, useComboBox, useFilter} from 'react-aria';
1313
import {ButtonContext} from './Button';
1414
import {Collection, ComboBoxState, Node, useComboBoxState} from 'react-stately';
15-
import {CollectionDocumentContext, useCollectionDocument} from './Collection';
16-
import {ContextValue, forwardRefType, Hidden, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
15+
import {CollectionBuilder} from './Collection';
16+
import {ContextValue, forwardRefType, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
1717
import {FieldErrorContext} from './FieldError';
1818
import {filterDOMProps, useResizeObserver} from '@react-aria/utils';
1919
import {FormContext} from './Form';
@@ -67,35 +67,25 @@ export const ComboBoxStateContext = createContext<ComboBoxState<any> | null>(nul
6767

6868
function ComboBox<T extends object>(props: ComboBoxProps<T>, ref: ForwardedRef<HTMLDivElement>) {
6969
[props, ref] = useContextProps(props, ref, ComboBoxContext);
70-
let {collection, document} = useCollectionDocument();
7170
let {children, isDisabled = false, isInvalid = false, isRequired = false} = props;
72-
children = useMemo(() => (
73-
typeof children === 'function'
74-
? children({
75-
isOpen: false,
76-
isDisabled,
77-
isInvalid,
78-
isRequired,
79-
defaultChildren: null
80-
})
81-
: children
82-
), [children, isDisabled, isInvalid, isRequired]);
71+
let content = useMemo(() => (
72+
<ListBoxContext.Provider value={{items: props.items ?? props.defaultItems}}>
73+
{typeof children === 'function'
74+
? children({
75+
isOpen: false,
76+
isDisabled,
77+
isInvalid,
78+
isRequired,
79+
defaultChildren: null
80+
})
81+
: children}
82+
</ListBoxContext.Provider>
83+
), [children, isDisabled, isInvalid, isRequired, props.items, props.defaultItems]);
8384

8485
return (
85-
<>
86-
{/* Render a hidden copy of the children so that we can build the collection even when the popover is not open.
87-
* This should always come before the real DOM content so we have built the collection by the time it renders during SSR. */}
88-
<Hidden>
89-
<Provider
90-
values={[
91-
[CollectionDocumentContext, document],
92-
[ListBoxContext, {items: props.items ?? props.defaultItems}]
93-
]}>
94-
{children}
95-
</Provider>
96-
</Hidden>
97-
<ComboBoxInner props={props} collection={collection} comboBoxRef={ref} />
98-
</>
86+
<CollectionBuilder content={content}>
87+
{collection => <ComboBoxInner props={props} collection={collection} comboBoxRef={ref} />}
88+
</CollectionBuilder>
9989
);
10090
}
10191

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
import {AriaGridListProps, DraggableItemResult, DragPreviewRenderer, DropIndicatorAria, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridList, useGridListItem, useGridListSelectionCheckbox, useHover, useLocale, useVisuallyHidden} from 'react-aria';
1313
import {ButtonContext} from './Button';
1414
import {CheckboxContext} from './RSPContexts';
15-
import {Collection, DraggableCollectionState, DroppableCollectionState, ListState, Node, SelectionBehavior, useListState} from 'react-stately';
16-
import {CollectionProps, CollectionRendererContext, createLeafComponent, ItemRenderProps, useCollection} from './Collection';
15+
import {Collection, CollectionBuilder, CollectionProps, CollectionRendererContext, createLeafComponent, ItemRenderProps} from './Collection';
1716
import {ContextValue, DEFAULT_SLOT, forwardRefType, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
1817
import {DragAndDropContext, DragAndDropHooks, DropIndicator, DropIndicatorContext, DropIndicatorProps} from './useDragAndDrop';
18+
import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, useListState} from 'react-stately';
1919
import {filterDOMProps, useObjectRef} from '@react-aria/utils';
2020
import {HoverEvents, Key, LinkDOMProps} from '@react-types/shared';
2121
import {ListStateContext} from './ListBox';
@@ -74,18 +74,17 @@ export const GridListContext = createContext<ContextValue<GridListProps<any>, HT
7474
function GridList<T extends object>(props: GridListProps<T>, ref: ForwardedRef<HTMLDivElement>) {
7575
// Render the portal first so that we have the collection by the time we render the DOM in SSR.
7676
[props, ref] = useContextProps(props, ref, GridListContext);
77-
let {collection, portal} = useCollection(props);
77+
7878
return (
79-
<>
80-
{portal}
81-
<GridListInner props={props} collection={collection} gridListRef={ref} />
82-
</>
79+
<CollectionBuilder content={<Collection {...props} />}>
80+
{collection => <GridListInner props={props} collection={collection} gridListRef={ref} />}
81+
</CollectionBuilder>
8382
);
8483
}
8584

8685
interface GridListInnerProps<T extends object> {
8786
props: GridListProps<T>,
88-
collection: Collection<Node<T>>,
87+
collection: ICollection<Node<object>>,
8988
gridListRef: RefObject<HTMLDivElement | null>
9089
}
9190

0 commit comments

Comments
 (0)