|
11 | 11 | */
|
12 | 12 | import {CollectionBase, DropTargetDelegate, Key, LayoutDelegate} from '@react-types/shared';
|
13 | 13 | import {createPortal} from 'react-dom';
|
14 |
| -import {forwardRefType, StyleProps} from './utils'; |
| 14 | +import {forwardRefType, Hidden, StyleProps} from './utils'; |
15 | 15 | import {Collection as ICollection, Node, SelectionBehavior, SelectionMode, SectionProps as SharedSectionProps} from 'react-stately';
|
16 | 16 | 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'; |
18 | 18 | import {useLayoutEffect} from '@react-aria/utils';
|
19 | 19 | import {useSyncExternalStore as useSyncExternalStoreShim} from 'use-sync-external-store/shim/index.js';
|
20 | 20 |
|
@@ -274,7 +274,7 @@ class BaseNode<T> {
|
274 | 274 | * A mutable element node in the fake DOM tree. It owns an immutable
|
275 | 275 | * Collection Node which is copied on write.
|
276 | 276 | */
|
277 |
| -export class ElementNode<T> extends BaseNode<T> { |
| 277 | +class ElementNode<T> extends BaseNode<T> { |
278 | 278 | nodeType = 8; // COMMENT_NODE (we'd use ELEMENT_NODE but React DevTools will fail to get its dimensions)
|
279 | 279 | node: NodeValue<T>;
|
280 | 280 | private _index: number = 0;
|
@@ -503,7 +503,7 @@ export class BaseCollection<T> implements ICollection<Node<T>> {
|
503 | 503 | * A mutable Document in the fake DOM. It owns an immutable Collection instance,
|
504 | 504 | * which is lazily copied on write during updates.
|
505 | 505 | */
|
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> { |
507 | 507 | nodeType = 11; // DOCUMENT_FRAGMENT_NODE
|
508 | 508 | ownerDocument = this;
|
509 | 509 | dirtyNodes: Set<BaseNode<T>> = new Set();
|
@@ -709,16 +709,41 @@ export function useCollectionChildren<T extends object>(props: CachedChildrenOpt
|
709 | 709 | }
|
710 | 710 |
|
711 | 711 | const ShallowRenderContext = createContext(false);
|
| 712 | +const CollectionDocumentContext = createContext<Document<any, BaseCollection<any>> | null>(null); |
712 | 713 |
|
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 |
716 | 718 | }
|
717 | 719 |
|
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); |
722 | 747 | }
|
723 | 748 |
|
724 | 749 | interface CollectionDocumentResult<T, C extends BaseCollection<T>> {
|
@@ -747,10 +772,10 @@ const useSyncExternalStore = typeof React['useSyncExternalStore'] === 'function'
|
747 | 772 | ? React['useSyncExternalStore']
|
748 | 773 | : useSyncExternalStoreFallback;
|
749 | 774 |
|
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> { |
751 | 776 | // The document instance is mutable, and should never change between renders.
|
752 | 777 | // 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)); |
754 | 779 | let subscribe = useCallback((fn: () => void) => document.subscribe(fn), [document]);
|
755 | 780 | let getSnapshot = useCallback(() => {
|
756 | 781 | let collection = document.getCollection();
|
@@ -779,27 +804,6 @@ export function useCollectionDocument<T extends object, C extends BaseCollection
|
779 | 804 | }
|
780 | 805 |
|
781 | 806 | 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 |
| -} |
803 | 807 |
|
804 | 808 | export interface ItemRenderProps {
|
805 | 809 | /**
|
@@ -919,7 +923,30 @@ export function Collection<T extends object>(props: CollectionProps<T>): JSX.Ele
|
919 | 923 | let ctx = useContext(CollectionContext)!;
|
920 | 924 | props = mergeProps(ctx, props);
|
921 | 925 | 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); |
923 | 950 | }
|
924 | 951 |
|
925 | 952 | 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 {
|
976 | 1003 | CollectionBranch: React.ComponentType<CollectionBranchProps>
|
977 | 1004 | }
|
978 | 1005 |
|
979 |
| -const DefaultCollectionRenderer: CollectionRenderer = { |
| 1006 | +export const DefaultCollectionRenderer: CollectionRenderer = { |
980 | 1007 | CollectionRoot({collection}) {
|
981 | 1008 | return useCachedChildren({
|
982 | 1009 | items: collection,
|
|
0 commit comments