---
name: context-and-collection
type: core
domain: composition
requires: [loke-ui]
description: >
  Building compound components with createContext (error-throwing consumer hook)
  and createContextScope (nested composition with scope threading).
  createCollection for DOM-ordered item tracking via data attributes +
  ResizeObserver pattern. Scope threading required for components that may be
  nested within themselves (Accordion inside Accordion, nested Menus, etc.).
---

# Context and Collection

## Setup

```tsx
import { createContext } from "@loke/ui/context";

// Returns [Provider, useContext]
const [ToastProvider, useToastContext] = createContext<{
  duration: number;
  onClose: () => void;
}>("Toast");

function Toast({ duration, onClose, children }: {
  duration: number;
  onClose: () => void;
  children: React.ReactNode;
}) {
  return (
    <ToastProvider duration={duration} onClose={onClose}>
      {children}
    </ToastProvider>
  );
}

function ToastClose() {
  const { onClose } = useToastContext("ToastClose");
  return <button onClick={onClose}>Dismiss</button>;
}
```

If `ToastClose` renders outside `ToastProvider`, the hook throws:
> `` `ToastClose` must be used within `Toast` ``

## Core Patterns

### createContext with optional default

Pass a `defaultContext` as the second argument to make the context optional (hook returns default instead of throwing when no provider is found).

```tsx
import { createContext } from "@loke/ui/context";

const [SizeProvider, useSizeContext] = createContext<{ size: "sm" | "md" | "lg" }>(
  "Button",
  { size: "md" }, // default — hook never throws
);

// Can be used without a provider; returns { size: "md" }
function ButtonIcon() {
  const { size } = useSizeContext("ButtonIcon");
  return <span data-size={size} />;
}
```

### createContextScope for nested composition

Use `createContextScope` when a component may be nested inside itself, or when two components share infrastructure (e.g., Popover + Tooltip both using Popper). Each instance gets its own scope, preventing context bleed between nested copies.

```tsx
import { createContextScope, type Scope } from "@loke/ui/context";

const ACCORDION_NAME = "Accordion";

// Returns [createContext fn for this scope, createScope fn]
const [createAccordionContext, createAccordionScope] =
  createContextScope(ACCORDION_NAME);

type AccordionContextValue = {
  value: string[];
  onItemOpen: (value: string) => void;
};

const [AccordionProvider, useAccordionContext] =
  createAccordionContext<AccordionContextValue>(ACCORDION_NAME);

// Export scope creator so consumers can compose it
export { createAccordionScope };

// Scope prop threads isolation through the tree
type ScopedProps<P> = P & { __scopeAccordion?: Scope };

function Accordion(props: ScopedProps<{
  value: string[];
  onValueChange: (v: string[]) => void;
  children: React.ReactNode;
}>) {
  const { __scopeAccordion, value, onValueChange, children } = props;
  return (
    <AccordionProvider
      value={value}
      onItemOpen={(v) => onValueChange([...value, v])}
      scope={__scopeAccordion}
    >
      {children}
    </AccordionProvider>
  );
}

function AccordionItem(props: ScopedProps<{ value: string; children: React.ReactNode }>) {
  const { __scopeAccordion, value, children } = props;
  const { onItemOpen } = useAccordionContext("AccordionItem", __scopeAccordion);
  return (
    <div onClick={() => onItemOpen(value)}>{children}</div>
  );
}
```

### createCollection for DOM-ordered items

`createCollection` tracks registered items in DOM order rather than insertion order. This is critical for roving focus and keyboard navigation where visual order must match traversal order.

```tsx
import { createCollection } from "@loke/ui/collection";
import { createContextScope } from "@loke/ui/context";

// ItemData is extra metadata stored per item
const [Collection, useCollection, createCollectionScope] = createCollection<
  HTMLButtonElement,
  { disabled: boolean; value: string }
>("ListBox");

const [createListBoxContext, createListBoxScope] = createContextScope(
  "ListBox",
  [createCollectionScope],
);

// In the root component — wrap with Collection.Provider + Collection.Slot
function ListBox({ children, __scopeListBox }: ScopedProps<{ children: React.ReactNode }>) {
  return (
    <Collection.Provider scope={__scopeListBox}>
      <Collection.Slot scope={__scopeListBox}>
        <div role="listbox">{children}</div>
      </Collection.Slot>
    </Collection.Provider>
  );
}

// In each item — wrap with Collection.ItemSlot passing metadata
function ListBoxItem({
  value,
  disabled = false,
  children,
  __scopeListBox,
}: ScopedProps<{ value: string; disabled?: boolean; children: React.ReactNode }>) {
  return (
    <Collection.ItemSlot scope={__scopeListBox} value={value} disabled={disabled}>
      <button role="option" disabled={disabled}>
        {children}
      </button>
    </Collection.ItemSlot>
  );
}

// Consumer — getItems() returns items sorted by DOM position
function useListBoxItems(__scopeListBox: Scope) {
  const getItems = useCollection(__scopeListBox);
  return getItems(); // [{ ref, value, disabled }, ...] in DOM order
}
```

## Common Mistakes

### 1. Using React.createContext directly instead of the library's createContext

`React.createContext` does not provide the error-throwing hook or scope threading. Context created with `React.createContext` will silently return `undefined` when used outside a provider (or whatever default you set), instead of throwing a descriptive error.

```tsx
// WRONG — silent undefined, no helpful error
const MyCtx = React.createContext<{ value: string } | undefined>(undefined);
function useMyContext() {
  return React.useContext(MyCtx); // returns undefined silently
}

// CORRECT — throws: "`MyConsumer` must be used within `MyComponent`"
import { createContext } from "@loke/ui/context";
const [MyProvider, useMyContext] = createContext<{ value: string }>("MyComponent");
```

Source: `createContext.tsx` — `useContext` checks for `undefined` and throws with the component name.

### 2. Missing scope threading in nested components

When using `createContextScope`, you must pass the `__scope*` prop through every level of the tree. Omitting it means nested instances of the same component share a single context, causing the inner component to read state from the outer one.

```tsx
// WRONG — AccordionItem reads outer Accordion's context when nested
function AccordionItem({ value, children }: { value: string; children: React.ReactNode }) {
  const ctx = useAccordionContext("AccordionItem"); // no scope
  // ...
}

// CORRECT — scope isolates each Accordion instance
function AccordionItem(props: ScopedProps<{ value: string; children: React.ReactNode }>) {
  const { __scopeAccordion, value, children } = props;
  const ctx = useAccordionContext("AccordionItem", __scopeAccordion);
  // ...
}
```

Source: `createContext.tsx` — `createContextScope` creates per-instance context slots indexed by scope object identity.

### 3. Assuming insertion order equals DOM order in collections

Items register in `useEffect`, which fires after paint. The order of effects does not match DOM order when items are conditionally rendered or reordered. `useCollection` sorts by DOM position using `querySelectorAll`, not by registration order.

```tsx
// WRONG — assumes items[0] is the first registered item
const getItems = useCollection(scope);
const firstItem = getItems()[0]; // correct DOM order, not insertion order

// CORRECT mental model: always call getItems() at use time; it queries the DOM
function handleKeyDown() {
  const items = getItems(); // fresh DOM-ordered snapshot
  const nextItem = items[currentIndex + 1];
  nextItem?.ref.current?.focus();
}
```

Source: `collection.tsx` — `getItems` uses `querySelectorAll("[data-squared-collection-item]")` to sort by DOM position at call time.
