Collection Utility
Create scoped collections with Provider/Slot/ItemSlot and read them in DOM order via useCollection.
Collection Utility
The Collection utility helps you build component groups (lists, menus, tabs, toolbars, etc.) that:
- register items declaratively,
- preserve DOM order even across fragments, wrappers, and conditional rendering,
- and expose a stable API to read items and their metadata.
It does this with a small set of primitives you assemble in your own components.
Think of it as an internal data layer for compound widgets: you build your Provider, Slot, ItemSlot, and consume the ordered items with useCollection.
Why use it?
- Keeps item order consistent with the DOM, even through fragments and nested trees
- Helps you build accessible, data-driven UIs (menus, tabs, lists) with minimal boilerplate
- Works with memoized items and dynamic insertion/removal
API
createCollection
Creates a collection with scoped context and registration primitives.
import { createCollection } from '@loke/ui/collection';
// Typed example:
const [
Collection, // { Provider, Slot, ItemSlot }
useCollection, // hook to read ordered items
createScope, // factory to create a new scope instance
] = createCollection<HTMLLIElement, { disabled?: boolean }>('List');Prop
Type
Provider/Slot/ItemSlot props
All three components participate in scoping. Pass the same scope object returned by createScope() (or reuse the same reference) so registration and lookups happen within the same collection.
Prop
Type
Prop
Type
Prop
Type
Usage
1) Build a List and Item using the primitives
import * as React from 'react';
import { createCollection } from '@loke/ui/collection';
type ItemData = { disabled?: boolean };
// Build a typed collection for <li> elements with optional "disabled" data
const [Collection, useCollection, createListScope] =
createCollection<HTMLLIElement, ItemData>('List');
const ListScopeContext = React.createContext<ReturnType<typeof createListScope> | null>(null);
function List({ children }: { children: React.ReactNode }) {
const scope = React.useMemo(() => createListScope(), []);
return (
<ListScopeContext.Provider value={scope}>
<Collection.Provider scope={scope}>
<ul>
<Collection.Slot scope={scope}>{children}</Collection.Slot>
</ul>
</Collection.Provider>
</ListScopeContext.Provider>
);
}
type ItemProps = React.ComponentPropsWithoutRef<'li'> & { disabled?: boolean };
function Item({ disabled, ...props }: ItemProps) {
const scope = React.useContext(ListScopeContext)!;
return (
<Collection.ItemSlot scope={scope} disabled={disabled}>
<li {...props} style={{ ...props.style, opacity: disabled ? 0.4 : 1 }} />
</Collection.ItemSlot>
);
}
// Helper to log ordered items
function LogItems({ name = 'items' }: { name?: string }) {
const scope = React.useContext(ListScopeContext)!;
const getItems = useCollection(scope);
React.useEffect(() => {
const items = getItems();
// You can inspect items[n].ref.current and your ItemData (e.g. disabled)
// For demo, just log order + disabled flag
// eslint-disable-next-line no-console
console.log(
name,
items.map((i) => ({ disabled: (i as any).disabled })),
);
}, [getItems, name]);
return null;
}2) Basic list
<List>
<Item>Red</Item>
<Item disabled>Green</Item>
<Item>Blue</Item>
<LogItems />
</List>3) With fragments/wrappers
Works across fragments and custom wrapper components. Order matches DOM order.
<>
<List>
<>
<Item>France</Item>
<Item disabled>UK</Item>
<Item>Spain</Item>
</>
<LogItems />
</List>
</>const Tomato = () => <Item style={{ color: 'tomato' }}>Tomato</Item>;
<List>
<Item>Red</Item>
<Item disabled>Green</Item>
<Tomato />
<Item>Blue</Item>
<LogItems />
</List>4) Dynamic insertion/removal
Items can be added/removed, order stays correct.
function DynamicExample() {
const [hasTomato, setHasTomato] = React.useState(false);
return (
<>
<button type="button" onClick={() => setHasTomato((s) => !s)}>
{hasTomato ? 'Remove' : 'Add'} Tomato
</button>
<List>
<Item>Red</Item>
{hasTomato && <Item style={{ color: 'tomato' }}>Tomato</Item>}
<Item disabled>Green</Item>
<Item>Blue</Item>
<LogItems />
</List>
</>
);
}5) Nested collections
Scopes keep nested lists independent.
<List>
<Item>1</Item>
<Item>
2
<List>
<Item>2.1</Item>
<Item>2.2</Item>
<Item>2.3</Item>
<LogItems name="items inside 2" />
</List>
</Item>
<Item>3</Item>
<LogItems name="top-level items" />
</List>Patterns and tips
- Use a dedicated React context to keep and share the current scope instance (see
ListScopeContextabove). - The getter returned by useCollection(scope) is stable; call it in effects or event handlers when you need a fresh snapshot of items.
- Add any custom data to ItemSlot (e.g.
disabled,value,id) so consumers have everything they need. - For keyboard-roving or selection state, pair the collection with a roving-focus hook or your own selection manager.
Troubleshooting
- “Items not registering” — ensure the same scope is passed to Provider/Slot/ItemSlot.
- “Wrong order” — make sure all items are rendered under the same Collection.Slot and you didn’t accidentally render siblings outside it.
- “Ref is null” — wait until after mount (e.g., inside an effect) to read item.ref.current.
Context Utility
Create contexts and scoped contexts with composable scopes for complex component libraries.
Focus Guards Utility
Inject invisible focus guards at the document edges so focus transitions can be captured consistently across your app (useful for modals, overlays, and complex focus management).