diff --git a/.github/copilot-instructions-public-api.md b/.github/copilot-instructions-public-api.md new file mode 100644 index 00000000..4681ebb7 --- /dev/null +++ b/.github/copilot-instructions-public-api.md @@ -0,0 +1,1102 @@ +# React Facet Public API Guide + +> **Note**: This is a streamlined public API reference for React Facet users. For the comprehensive internal guide (including repository structure, testing patterns, and contributor workflows), see [`copilot-instructions.md`](./copilot-instructions.md). + +## Overview + +**React Facet** (`@react-facet`) is an observable-based state management system designed for performant game UIs built in React. It bypasses React reconciliation for leaf node updates (styles, text content, attributes) to achieve game-level performance while maintaining React's developer experience. + +### Core Philosophy + +React Facet is designed for: + +- **Game UI development** using embedded web technologies (Coherent Labs' Gameface) +- **Performance-critical applications** with fixed frame budgets +- **Frequent UI updates** without triggering React re-renders + +--- + +## ⚠️ Critical Rules (Must Follow) + +### 1. 🚨 Always Check for NO_VALUE After useFacetUnwrap + +`useFacetUnwrap` and setter callbacks return `T | NO_VALUE`, not just `T`. Always check before using: + +```typescript +// ❌ WRONG - TypeScript ERROR! +const value = useFacetUnwrap(numberFacet) +const doubled = value * 2 // Error: NO_VALUE is not a number + +// βœ… CORRECT - Check for NO_VALUE first +const value = useFacetUnwrap(numberFacet) +if (value !== NO_VALUE) { + const doubled = value * 2 +} + +// βœ… CORRECT - In setter callbacks +const [items, setItems] = useFacetState([]) +setItems((current) => (current !== NO_VALUE ? [...current, 'new'] : ['new'])) +``` + +### 2. 🚨 Minimize useFacetUnwrap Usage + +`useFacetUnwrap` causes React re-renders, defeating the entire performance benefit of facets: + +```typescript +// ❌ WRONG - Causes re-renders! +const value = useFacetUnwrap(facet) +return
{value}
+ +// βœ… CORRECT - Use fast-text, no re-renders +return + +// ❌ WRONG - Unwrapping for conditional rendering +const isVisible = useFacetUnwrap(isVisibleFacet) +if (isVisible !== NO_VALUE && !isVisible) return null + +// βœ… CORRECT - Use Mount component + + + +``` + +### 3. 🚨 Two Dependency Arrays Pattern + +Facet hooks use TWO dependency arrays: + +1. **First array**: Non-facet dependencies (props, local vars, functions) +2. **Second array**: Facet dependencies + +```typescript +// ❌ WRONG - Missing multiplier in first array +const multiplier = props.multiplier +const result = useFacetMap( + (value) => value * multiplier, + [], // ❌ Missing: [multiplier] + [valueFacet], +) + +// βœ… CORRECT - Include all non-facet dependencies +const result = useFacetMap( + (value) => value * multiplier, + [multiplier], // βœ… Non-facet dependencies + [valueFacet], // βœ… Facet dependencies +) +``` + +--- + +## Core Concepts + +### What is a Facet? + +A **Facet** is an observable state container that updates over time without triggering React re-renders. + +```typescript +interface Facet { + get: () => T | NO_VALUE + observe: (listener: (value: T) => void) => Unsubscribe +} + +interface WritableFacet extends Facet { + set: (value: T) => void + setWithCallback: (callback: (previousValue: T | NO_VALUE) => T | NO_VALUE) => void +} +``` + +### Key Characteristics + +- **Observable**: Components subscribe to facets and update when values change +- **Composable**: Derive facets from other facets using transformation functions +- **Performant**: Updates bypass React reconciliation with `fast-*` components +- **Type-safe**: Full TypeScript support with type inference + +--- + +## Public API Reference + +### Package: `@react-facet/core` + +#### Creating Facets + +**`useFacetState(initialValue: T): [Facet, Setter]`** + +Creates local component state as a facet. The facet reference is stable across re-renders. + +```typescript +const [counterFacet, setCounter] = useFacetState(0) + +// Direct value +setCounter(42) + +// Callback form - ALWAYS check for NO_VALUE +setCounter((current) => (current !== NO_VALUE ? current + 1 : 1)) +``` + +**`useFacetWrap(value: T | Facet): Facet`** + +Wraps a value or facet prop into a facet. Creates new facet instance when value changes. + +```typescript +type Props = { + count: number | Facet // Accept either value or facet +} + +const Component = ({ count }: Props) => { + const countFacet = useFacetWrap(count) + return +} +``` + +**`useFacetWrapMemo(value: T | Facet, equalityCheck?): Facet`** + +Like `useFacetWrap` but maintains stable facet reference. Best for frequently changing props. + +```typescript +// Stable facet reference even when prop changes +const stableFacet = useFacetWrapMemo(propValue) +``` + +**When to use which:** + +- **`useFacetWrap`**: Default choice, simpler implementation +- **`useFacetWrapMemo`**: When facet reference stability matters or props change frequently + +#### Deriving Facets + +**`useFacetMap(fn, dependencies, facets, equalityCheck?): Facet`** + +Transforms facet values. Lightweight, best for simple transformations. + +```typescript +// Single facet +const doubled = useFacetMap((n) => n * 2, [], [numberFacet]) + +// Multiple facets +const fullName = useFacetMap((first, last) => `${first} ${last}`, [], [firstNameFacet, lastNameFacet]) + +// With local dependencies +const scaled = useFacetMap( + (value) => value * scale, + [scale], // Non-facet dependency + [valueFacet], +) + +// With equality check for objects +const combined = useFacetMap((a, b) => ({ a, b }), [], [facetA, facetB], shallowObjectEqualityCheck) +``` + +**`useFacetMemo(fn, dependencies, facets, equalityCheck?): Facet`** + +Like `useFacetMap` but caches result across all subscribers. Use for expensive computations or many subscribers. + +```typescript +// Expensive computation cached for all subscribers +const expensive = useFacetMemo((data) => heavyCalculation(data), [], [dataFacet]) +``` + +**When to use which:** + +- **`useFacetMap`**: Default choice (lightweight, fast initialization) +- **`useFacetMemo`**: When you have 3+ subscribers OR expensive computations + +#### Side Effects + +**`useFacetEffect(effect, dependencies, facets): void`** + +Runs side effects when facet values change. Like `useEffect` but for facets. + +```typescript +useFacetEffect( + (health) => { + if (health < 20) { + playWarningSound() + } + }, + [], + [healthFacet], +) +``` + +**`useFacetLayoutEffect(effect, dependencies, facets): void`** + +Synchronous version of `useFacetEffect`. Like `useLayoutEffect` but for facets. + +```typescript +useFacetLayoutEffect( + (dimensions) => { + measureAndUpdateLayout(dimensions) + }, + [], + [dimensionsFacet], +) +``` + +#### Callbacks + +**`useFacetCallback(callback, dependencies, facets, defaultReturn?): (...args) => M`** + +Creates callbacks that depend on facet values. The callback stays stable while accessing current facet values. + +```typescript +const handleSubmit = useFacetCallback( + (username, password) => () => { + submitLoginForm(username, password) + }, + [], + [usernameFacet, passwordFacet], +) + +// With parameters +const selectItem = useFacetCallback( + (selectedId) => (itemId: string) => { + setSelectedId(selectedId === itemId ? null : itemId) + }, + [], + [selectedIdFacet], +) +``` + +**When NOT to use:** If you only need to update facet state, use regular callback with setter's callback form instead. + +#### Unwrapping (Use Sparingly!) + +**`useFacetUnwrap(facet: Facet): T | NO_VALUE`** + +Converts facet to regular React state. **WARNING: Causes re-renders!** + +```typescript +// ⚠️ Only use when necessary (e.g., third-party components) +const value = useFacetUnwrap(facet) + +// ALWAYS check for NO_VALUE before using +if (value !== NO_VALUE) { + return +} +``` + +**When to use:** + +- Passing to non-facet-aware third-party components (though refactoring is preferred) +- Last resort only - prefer `fast-*` components or `Mount`/`With` for conditional rendering + +#### Transitions (React 18+) + +**`useFacetTransition(): [boolean, (fn: () => void) => void]`** + +Marks facet updates as low-priority transitions. Keeps UI responsive during expensive operations. + +```typescript +const [isPending, startTransition] = useFacetTransition() + +const handleHeavyUpdate = () => { + setInputFacet(newValue) // High priority - immediate + + startTransition(() => { + // Low priority - can be interrupted + const results = expensiveComputation(newValue) + setResultsFacet(results) + }) +} + +return ( +
+ {isPending &&
Processing...
} + +
+) +``` + +**`startFacetTransition(fn: () => void): void`** + +Function API for transitions. Use outside components or when you don't need pending state. + +```typescript +// In utility functions +export const loadData = (setData: (data: string[]) => void, newData: string[]) => { + startFacetTransition(() => { + setData(newData) + }) +} +``` + +**Key characteristics:** + +- The `startTransition` callback from `useFacetTransition` is **stable** - don't include in dependency arrays +- Always wrap risky computations in try-catch blocks +- Transitions can be nested + +#### Other Utilities + +**`useFacetRef(facet: Facet): RefObject`** + +Creates a ref that updates with facet value. Useful for accessing facet values in imperative code. + +```typescript +const countRef = useFacetRef(countFacet) +// Access current value: countRef.current +``` + +**`useFacetReducer(reducer, initialState, equalityCheck?): [Facet, Dispatch]`** - **NOT RECOMMENDED** + +Parallel to React's `useReducer`, but returns a facet as the value. This hook is rarely needed in practice - prefer `useFacetState` with the setter's callback form instead. + +```typescript +// ⚠️ NOT RECOMMENDED - Use useFacetState instead +type State = { count: number } +type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' } + +const reducer = (state: Option, action: Action): Option => { + if (state === NO_VALUE) return { count: 0 } + + switch (action.type) { + case 'increment': + return { count: state.count + 1 } + case 'decrement': + return { count: state.count - 1 } + case 'reset': + return { count: 0 } + } +} + +const [stateFacet, dispatch] = useFacetReducer(reducer, { count: 0 }) + +// Usage +dispatch({ type: 'increment' }) +``` + +**`useFacetPropSetter(facet: WritableFacet, prop: Prop): (value: T[Prop]) => void`** - **NOT RECOMMENDED** + +Returns a setter function for a specific property of a facet object. In practice, using the setter's callback form is more straightforward. + +```typescript +// ⚠️ NOT RECOMMENDED - Use setter callback form instead +type FormData = { + username: string + email: string +} + +const [formFacet, setForm] = useFacetState({ + username: '', + email: '', +}) + +// Create setters for individual properties +const setUsername = useFacetPropSetter(formFacet, 'username') +const setEmail = useFacetPropSetter(formFacet, 'email') + +// Use in components + setUsername(e.target.value)} /> + setEmail(e.target.value)} /> + +// βœ… BETTER - Use setter callback form directly + setForm(current => + current !== NO_VALUE ? { ...current, username: e.target.value } : { username: e.target.value, email: '' } +)} /> +``` + +**`createFacetContext(): Context>`** + +Creates React context for facets. + +```typescript +const PlayerContext = createFacetContext() + +// Provider + + + + +// Consumer +const playerFacet = useContext(PlayerContext) +``` + +#### Advanced (Use with Caution) + +**`createFacet({ initialValue, startSubscription, equalityCheck? }): WritableFacet`** + +Low-level facet creation. **Primarily for testing** - use `useFacetState` or `useFacetWrap` in components. + +```typescript +// Testing/mocking only +const mockFacet = createFacet({ + initialValue: 'test-value', + startSubscription: (update) => { + // Custom subscription logic + return () => {} // cleanup + }, +}) +``` + +**`createStaticFacet(value: T): Facet`** + +Creates a facet that never changes. Useful for static values that need facet interface. + +**`createReadOnlyFacet(facet: Facet): Facet`** + +Creates a read-only view of a writable facet. + +#### Components + +**`{children}`** + +Conditionally mount children based on facet value. **Use this instead of `useFacetUnwrap` for conditional rendering.** + +```typescript + + + +``` + +**`{(itemFacet, index) => }`** + +Renders list from array facet. Each item gets its own facet. + +```typescript +;{(itemFacet, index) => } + +// Separate component to use hooks (Rules of Hooks) +const ItemRow = ({ itemFacet }: { itemFacet: Facet }) => { + const nameFacet = useFacetMap((item) => item.name, [], [itemFacet]) + return +} +``` + +**`{(value) =>
{value}
}
`** + +Conditional rendering with access to unwrapped value. + +```typescript +{(selectedId) => selectedId &&
Selected: {selectedId}
}
+``` + +**`{(value) => }`** + +Extracts plain value from facet with controlled re-render scope. Uses `useFacetUnwrap` internally but limits re-renders to its children. + +```typescript +// Basic usage - passes plain value to children +{(name) =>
Hello, {name}!
}
+ +// Multi-branch conditional - better than two opposing Mount components +{(cond) => (cond ? : )} + +// Third-party component integration +{(value) => } +``` + +**When to use:** + +- βœ… Interfacing with third-party components that accept plain values +- βœ… Multi-branch conditionals (prefer over multiple `Mount`s) +- βœ… When you need plain values but want controlled re-render scope + +**When NOT to use:** + +- ❌ For binding to DOM properties (use `fast-*` components instead) +- ❌ For simple boolean mounting (use `Mount` instead) +- ❌ When you can keep values as facets (maintain facet semantics) + +**`{(index, total) => }`** + +Renders children a specified number of times based on numeric facet. + +```typescript +// Basic usage +; + {(index, total) => ( +
+ Row {index} of {total} +
+ )} +
+ +// Dynamic count +const [countFacet, setCount] = useFacetState(3) +return ( +
+ {(index) =>
Item {index}
}
+ +
+) +``` + +**When to use:** + +- βœ… Repeating UI a variable number of times +- βœ… Simple numeric iteration with dynamic count +- βœ… When you don't have array data (just need N repetitions) + +**When NOT to use:** + +- ❌ For rendering lists from array data (use `Map` instead) +- ❌ When you need per-item facets (use `Map` with array facet) +- ❌ For static repetition (use regular array mapping) + +#### Equality Checks + +```typescript +import { + strictEqualityCheck, // For primitives & functions (type-safe) + shallowObjectEqualityCheck, // For objects with primitive values + shallowArrayEqualityCheck, // For arrays of primitives + defaultEqualityCheck, // Used automatically (performance optimized) +} from '@react-facet/core' +``` + +**Best practices:** + +- For primitives: Omit the equality check (uses optimized `defaultEqualityCheck`) +- For objects: Use `shallowObjectEqualityCheck` +- For arrays: Use `shallowArrayEqualityCheck` +- For type safety with primitives: Use `strictEqualityCheck` + +**Example:** + +```typescript +// Objects - always use equality check +const obj = useFacetMap((a, b) => ({ a, b }), [], [facetA, facetB], shallowObjectEqualityCheck) + +// Arrays - always use equality check +const arr = useFacetMap((a, b) => [a, b], [], [facetA, facetB], shallowArrayEqualityCheck) + +// Primitives - no need (optimized by default) +const doubled = useFacetMap((x) => x * 2, [], [xFacet]) +``` + +#### Special Values & Types + +```typescript +import { NO_VALUE } from '@react-facet/core' + +type Facet +type WritableFacet +type FacetProp = T | Facet +type Option = T | NO_VALUE +type Setter = (value: V | ((previousValue: Option) => Option)) => void +``` + +**NO_VALUE Retention Behavior:** + +When a mapping function or setter callback returns `NO_VALUE`, the facet **retains its previous value** (doesn't notify listeners). + +```typescript +// Clamping - stops updates at threshold +const clamped = useFacetMap((count) => (count < 5 ? count : NO_VALUE), [], [countFacet]) +// Shows: 0, 1, 2, 3, 4, 4, 4... (stuck at 4) + +// Conditional updates - prevent state changes +setCount((current) => { + if (current === NO_VALUE) return 0 + if (current >= 5) return NO_VALUE // Retains value 5 + return current + 1 +}) +``` + +--- + +### Package: `@react-facet/dom-fiber` + +Custom React renderer that natively understands facets. + +#### Mounting + +**`createRoot(container: HTMLElement): { render, unmount }`** + +Creates a root for rendering React Facet components. Drop-in replacement for `react-dom`. + +```typescript +import { createRoot } from '@react-facet/dom-fiber' + +const root = createRoot(document.getElementById('root')) +root.render() + +// Later +root.unmount() +``` + +#### fast-\* Components + +**Use `fast-*` components to bind facets to DOM properties without triggering reconciliation.** + +Available components: + +- **HTML**: `fast-div`, `fast-span`, `fast-p`, `fast-a`, `fast-img`, `fast-input`, `fast-textarea`, `fast-text` +- **SVG**: `fast-svg`, `fast-circle`, `fast-ellipse`, `fast-line`, `fast-path`, `fast-rect`, `fast-foreignObject`, `fast-use`, `fast-polyline`, `fast-polygon`, `fast-linearGradient`, `fast-radialGradient`, `fast-stop`, `fast-svg-text`, `fast-pattern` + +**Examples:** + +```typescript +// Mix facets and static values + + + + + +// Regular HTML for static structure +
+

Static Title

+ +
+``` + +**When to use:** + +- βœ… Use `fast-*` when binding facet values to DOM properties +- βœ… Use regular HTML for static content +- βœ… Mix both as needed + +**Gameface Optimizations:** + +Numeric CSS properties (faster than strings in Gameface): + +```typescript +{/* Properties ending in PX, VH, VW accept numbers */} +``` + +--- + +### Package: `@react-facet/shared-facet` + +Interface layer for game engine communication (Gameface integration). + +**`useSharedFacet(key: string, defaultValue?: T): [Facet, (value: T) => void]`** + +Creates a facet synchronized with game engine state. + +**`SharedFacetContext`** + +Context for shared facet provider configuration. + +--- + +## Common Patterns + +### Creating State + +```typescript +// Local component state +const [stateFacet, setState] = useFacetState(initialValue) + +// From props (flexible - accepts value or facet) +const facet = useFacetWrap(propValue) + +// From context +const sharedFacet = useContext(DataContext) +``` + +### Deriving State + +```typescript +// Simple transformation +const upper = useFacetMap((s) => s.toUpperCase(), [], [stringFacet]) + +// Multiple facets +const full = useFacetMap((first, last) => `${first} ${last}`, [], [firstFacet, lastFacet]) + +// With local variables +const scaled = useFacetMap((value) => value * multiplier, [multiplier], [valueFacet]) + +// Expensive computation +const result = useFacetMemo((data) => heavyCalculation(data), [], [dataFacet]) +``` + +### Rendering + +```typescript +// Direct facet binding - NO reconciliation + + + + + +// Conditional rendering + + + + +// List rendering + + {(itemFacet, index) => } + +``` + +### Side Effects Pattern + +```typescript +useFacetEffect( + (value) => { + // Side effect + return () => { + // Cleanup (optional) + } + }, + [], + [valueFacet], +) +``` + +### Event Handlers + +```typescript +// Regular callback with setter's callback form +const addItem = (newItem: string) => { + setItems((current) => (current !== NO_VALUE ? [...current, newItem] : [newItem])) +} + +// When you need to READ facet values (not just update) +const handleSubmit = useFacetCallback( + (username, password) => () => { + submitLoginForm(username, password) + }, + [], + [usernameFacet, passwordFacet], +) +``` + +--- + +## Common Pitfalls + +### 1. Forgetting NO_VALUE Checks + +```typescript +// ❌ WRONG +const value = useFacetUnwrap(facet) +const doubled = value * 2 + +// βœ… CORRECT +const value = useFacetUnwrap(facet) +if (value !== NO_VALUE) { + const doubled = value * 2 +} + +// ❌ WRONG - In setter callbacks +setItems((current) => [...current, newItem]) + +// βœ… CORRECT +setItems((current) => (current !== NO_VALUE ? [...current, newItem] : [newItem])) +``` + +### 2. Overusing useFacetUnwrap + +```typescript +// ❌ WRONG - Causes re-renders! +const value = useFacetUnwrap(facet) +return
{value}
+ +// βœ… CORRECT +return +``` + +### 3. Missing Dependencies + +```typescript +// ❌ WRONG +const multiplier = props.multiplier +const result = useFacetMap( + (value) => value * multiplier, + [], // Missing [multiplier] + [valueFacet], +) + +// βœ… CORRECT +const result = useFacetMap((value) => value * multiplier, [multiplier], [valueFacet]) +``` + +### 4. Wrong Equality Checks + +```typescript +// ❌ WRONG - Reference equality for objects +const obj = useFacetMap((a, b) => ({ a, b }), [], [facetA, facetB]) + +// βœ… CORRECT +const obj = useFacetMap((a, b) => ({ a, b }), [], [facetA, facetB], shallowObjectEqualityCheck) +``` + +### 5. Using fast-\* for Static Content + +```typescript +// ❌ WRONG - Unnecessary fast-div + + Static text + + +// βœ… CORRECT - Use regular HTML for static content +
+ Static text +
+ +// βœ… CORRECT - Use fast-* only for facet bindings + + + +``` + +### 6. Hooks in Conditionals, Loops, or Nested Functions + +```typescript +// ❌ WRONG - Hook in Map callback +; + {(itemFacet, index) => item.name, [], [itemFacet])} />} + + +// βœ… CORRECT - Separate component +const ItemRow = ({ itemFacet }) => { + const nameFacet = useFacetMap((item) => item.name, [], [itemFacet]) + return +} + +;{(itemFacet, index) => } +``` + +--- + +## Best Practices + +### Naming Conventions + +```typescript +// Facet variables: suffix with "Facet" +const counterFacet = useFacetState(0) +const playerHealthFacet = useContext(PlayerContext) + +// Setters: prefix with "set" +const [stateFacet, setState] = useFacetState(0) + +// Derived facets: descriptive names +const healthBarClass = useFacetMap(...) +const formattedDate = useFacetMap(...) +``` + +### Facet Reference Stability + +- **`useFacetState`**: Facet reference is **stable** (never changes) +- **`useFacetMap`/`useFacetMemo`**: Create **new facet** when dependencies change +- **`useFacetWrap`**: Creates **new facet** when wrapped value changes +- **`useFacetWrapMemo`**: Maintains **stable facet**, updates value internally + +### Performance Guidelines + +**Default to lightweight options:** + +- Use `useFacetMap` by default (fast initialization) +- Switch to `useFacetMemo` only when profiling shows bottleneck (3+ subscribers or expensive computation) +- Use `useFacetWrap` by default +- Switch to `useFacetWrapMemo` only when facet reference stability matters + +**Minimize re-renders:** + +- Avoid `useFacetUnwrap` except as last resort +- Use `Mount` for conditional rendering, not unwrapping +- Use `fast-*` components for all dynamic properties + +**Use transitions for heavy updates:** + +- Wrap expensive computations in `useFacetTransition` or `startFacetTransition` +- Keep input fields responsive by making them high-priority +- Always add try-catch blocks around risky computations in transitions + +### Type Safety + +```typescript +// Define types for facet data +type PlayerData = { + health: number + name: string +} + +const [playerFacet, setPlayer] = useFacetState({ + health: 100, + name: 'Steve', +}) + +// Type inference works for simple cases +const [count, setCount] = useFacetState(0) // inferred as number +``` + +--- + +## Quick Reference + +### Most Common Hooks + +```typescript +// State +useFacetState(initialValue): [Facet, Setter] +useFacetWrap(value: T | Facet): Facet +useFacetWrapMemo(value: T | Facet): Facet + +// Derivation +useFacetMap(fn, deps, facets, equalityCheck?): Facet +useFacetMemo(fn, deps, facets, equalityCheck?): Facet + +// Side effects +useFacetEffect(effect, deps, facets): void +useFacetLayoutEffect(effect, deps, facets): void + +// Callbacks +useFacetCallback(callback, deps, facets): (...args) => M + +// Utilities +useFacetUnwrap(facet): T | NO_VALUE // ⚠️ Use sparingly! +useFacetRef(facet): RefObject + +// Advanced (NOT RECOMMENDED) +useFacetReducer(reducer, initialState, equalityCheck?): [Facet, Dispatch
] // Not recommended +useFacetPropSetter(facet, prop): (value: T[Prop]) => void // Not recommended + +// Transitions +useFacetTransition(): [boolean, (fn: () => void) => void] +startFacetTransition(fn: () => void): void +``` + +### Core Components + +```typescript + +{(itemFacet, index) => } +{(value) =>
{value}
}
+{(value) => } +{(index, total) =>
Item {index}
}
+``` + +### Equality Check Functions + +```typescript +import { + strictEqualityCheck, + shallowObjectEqualityCheck, + shallowArrayEqualityCheck, + defaultEqualityCheck, +} from '@react-facet/core' +``` + +--- + +## Complete Examples + +### Example 1: Counter with Derived State + +```typescript +import { useFacetState, useFacetMap } from '@react-facet/core' +import { createRoot } from '@react-facet/dom-fiber' + +const Counter = () => { + const [countFacet, setCount] = useFacetState(0) + + const doubledFacet = useFacetMap((count) => count * 2, [], [countFacet]) + + const statusFacet = useFacetMap((count) => (count > 10 ? 'high' : 'low'), [], [countFacet]) + + return ( + + + + + + + + ) +} + +const root = createRoot(document.getElementById('root')) +root.render() +``` + +### Example 2: Form with Validation + +```typescript +import { useFacetState, useFacetMap, useFacetCallback } from '@react-facet/core' +import { shallowObjectEqualityCheck } from '@react-facet/core' + +type FormData = { + username: string + email: string +} + +const Form = () => { + const [formFacet, setForm] = useFacetState({ + username: '', + email: '', + }) + + const isValidFacet = useFacetMap((data) => data.username.length >= 3 && data.email.includes('@'), [], [formFacet]) + + const updateUsername = (username: string) => { + setForm((current) => (current !== NO_VALUE ? { ...current, username } : { username, email: '' })) + } + + const handleSubmit = useFacetCallback( + (data) => () => { + console.log('Submitting:', data) + }, + [], + [formFacet], + ) + + const isValid = useFacetUnwrap(isValidFacet) + + return ( +
+ updateUsername(e.target.value)} placeholder="Username" /> + +
+ ) +} +``` + +### Example 3: List with Conditional Rendering + +```typescript +import { useFacetState, useFacetMap, NO_VALUE } from '@react-facet/core' +import { Map, Mount } from '@react-facet/core' +import { shallowArrayEqualityCheck } from '@react-facet/core' + +type Item = { + id: string + name: string +} + +const ItemList = () => { + const [itemsFacet, setItems] = useFacetState([ + { id: '1', name: 'Item 1' }, + { id: '2', name: 'Item 2' }, + ]) + + const hasItemsFacet = useFacetMap((items) => items.length > 0, [], [itemsFacet]) + + const addItem = () => { + setItems((current) => + current !== NO_VALUE + ? [...current, { id: Date.now().toString(), name: 'New Item' }] + : [{ id: Date.now().toString(), name: 'New Item' }], + ) + } + + return ( +
+ + + + {(itemFacet, index) => } + +
+ ) +} + +const ItemRow = ({ itemFacet }: { itemFacet: Facet }) => { + const nameFacet = useFacetMap((item) => item.name, [], [itemFacet]) + return +} +``` + +--- + +## Resources + +- **Homepage**: +- **GitHub**: +- **Target Runtime**: Coherent Labs Gameface, Chromium Embedded Framework diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..09cc8deb --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,2344 @@ +# GitHub Copilot Instructions for Ore UI / React Facet + +> **Note**: This is the comprehensive internal guide for contributors. For a streamlined public API reference, see [`copilot-instructions-public-api.md`](./copilot-instructions-public-api.md). +> +> **Maintenance**: When updating this file, also update the public API version if changes affect public APIs, usage patterns, or best practices. Run `./scripts/check-copilot-instructions-sync.sh` and `./scripts/check-public-api-instructions-sync.sh` to validate both files. + +## Project Overview + +**Ore UI** is Mojang Studios' open-source collection of building blocks for constructing video game user interfaces using web standards. The flagship package is **React Facet** (`@react-facet`), an observable-based state management system designed for performant game UIs built in React. + +### Target Use Case + +- **Primary**: Game UI development using embedded web technologies (Coherent Labs' Gameface) +- **Games using this**: Minecraft Bedrock Edition, Minecraft Legends +- **Performance Requirements**: Fixed frame budget, optimized for slower devices + +### Core Philosophy + +React Facet bypasses React reconciliation for leaf node updates (styles, text content, attributes) to achieve game-level performance while maintaining React's developer experience. + +--- + +## ⚠️ Top 3 Critical Errors to Avoid + +Before diving into the details, be aware of these critical mistakes that **defeat the entire purpose of React Facet**: + +### 1. 🚨 CRITICAL: Forgetting to Check for NO_VALUE + +**Problem**: `useFacetUnwrap` and setter callbacks return `T | NO_VALUE`, not just `T`. Using the value without checking causes TypeScript errors and runtime bugs. + +```typescript +// ❌ WRONG - TypeScript ERROR! +const value = useFacetUnwrap(numberFacet) +const doubled = value * 2 // Error: NO_VALUE is not a number + +const [items, setItems] = useFacetState([]) +setItems((current) => [...current, 'new']) // Error: NO_VALUE is not spreadable + +// βœ… CORRECT - Always check for NO_VALUE +const value = useFacetUnwrap(numberFacet) +if (value !== NO_VALUE) { + const doubled = value * 2 // βœ“ Safe +} + +setItems((current) => (current !== NO_VALUE ? [...current, 'new'] : ['new'])) +``` + +**Remember**: + +- `useFacetUnwrap` β†’ always returns `T | NO_VALUE` +- Setter callbacks β†’ always receive `T | NO_VALUE` +- Check `!== NO_VALUE` before using the value + +### 2. 🚨 CRITICAL: Overusing useFacetUnwrap + +**Problem**: `useFacetUnwrap` causes React re-renders, defeating the entire performance benefit of facets. + +```typescript +// ❌ WRONG - Causes re-renders, defeats facet purpose! +const value = useFacetUnwrap(facet) +return
{value}
+ +// βœ… CORRECT - Use fast-text, no re-renders +return + +// ❌ WRONG - Unwrapping for conditional rendering +const isVisible = useFacetUnwrap(isVisibleFacet) +if (isVisible !== NO_VALUE && !isVisible) return null + +// βœ… CORRECT - Use Mount component + + + +``` + +**Rule**: Only use `useFacetUnwrap` as a **last resort** when interfacing with non-facet-aware third-party components. Otherwise, use `fast-*` components or facet-aware patterns. + +### 3. 🚨 CRITICAL: Missing Dependencies in First Array + +**Problem**: Facet hooks have TWO dependency arrays. Forgetting non-facet dependencies in the first array causes stale closures. + +```typescript +// ❌ WRONG - Missing multiplier in first array +const multiplier = props.multiplier +const result = useFacetMap( + (value) => value * multiplier, + [], // ❌ Missing: [multiplier] - will use stale value! + [valueFacet], +) + +// βœ… CORRECT - Include all non-facet dependencies +const result = useFacetMap( + (value) => value * multiplier, + [multiplier], // βœ… Non-facet dependencies here + [valueFacet], // βœ… Facet dependencies here +) +``` + +**Rule**: First array = non-facet deps (props, local vars, functions). Second array = facet deps. + +--- + +## Repository Structure + +This is a **yarn workspace monorepo** with the following organization: + +### Package Structure (`packages/@react-facet/`) + +``` +packages/@react-facet/ +β”œβ”€β”€ core/ # Core facet implementation +β”‚ └── src/ +β”‚ β”œβ”€β”€ facet/ # createFacet, createStaticFacet, createReadOnlyFacet +β”‚ β”œβ”€β”€ hooks/ # All useFacet* hooks +β”‚ β”œβ”€β”€ components/ # Map, Mount, With +β”‚ β”œβ”€β”€ mapFacets/ # Facet composition utilities +β”‚ β”œβ”€β”€ equalityChecks.ts # Equality check functions +β”‚ β”œβ”€β”€ createFacetContext.tsx # Context utilities +β”‚ └── types.ts # Core type definitions +β”‚ +β”œβ”€β”€ dom-fiber/ # Custom React renderer +β”‚ └── src/ +β”‚ β”œβ”€β”€ fast-* components # Facet-native DOM elements +β”‚ └── renderer implementation +β”‚ +β”œβ”€β”€ dom-fiber-testing-library/ # Testing utilities +β”‚ └── src/ +β”‚ └── render, act utilities +β”‚ +└── shared-facet/ # Gameface integration + └── src/ + └── useSharedFacet, Context +``` + +### Examples & Documentation + +``` +examples/ +└── benchmarking/ # Performance benchmarks and examples + +docs/ +β”œβ”€β”€ docs/ # Documentation content +β”‚ β”œβ”€β”€ api/ # API reference +β”‚ β”œβ”€β”€ game-ui-development/ # Gameface integration guides +β”‚ └── rendering/ # Renderer documentation +└── src/ # Docusaurus site source +``` + +### Import Patterns + +**Core imports** (hooks, utilities, types): + +```typescript +import { useFacetState, useFacetMap, useFacetEffect, NO_VALUE, shallowObjectEqualityCheck } from '@react-facet/core' +``` + +**Renderer imports** (for fast-\* components): + +```typescript +import { createRoot } from '@react-facet/dom-fiber' +// fast-div, fast-text, etc. are available globally when using dom-fiber +``` + +> **⚠️ CRITICAL:** Use `createRoot` (not `render`) for all new code. The `render` method is deprecated. + +**Testing imports**: + +```typescript +import { render, act } from '@react-facet/dom-fiber-testing-library' +``` + +> **Note:** In testing, `render` from the testing library is still used. The deprecated `render` is only from `@react-facet/dom-fiber` itself. + +**Gameface integration**: + +```typescript +import { useSharedFacet, SharedFacetContext } from '@react-facet/shared-facet' +``` + +### File Conventions + +- **Test files**: Co-located with source as `*.spec.ts` or `*.spec.tsx` +- **Type definitions**: Primarily in `types.ts` files within each package +- **Examples**: Component-based examples in each package's spec files +- **Documentation**: Markdown files in `docs/docs/` with frontmatter + +--- + +## What is a Facet? + +A **Facet** is an observable state container that updates over time without triggering React re-renders. Think of it as a reactive value that components can subscribe to directly. + +### Core Facet Interface + +```typescript +interface Facet { + get: () => T | NoValue + observe: (listener: (value: T) => void) => Unsubscribe +} + +interface WritableFacet extends Facet { + set: (value: T) => void + setWithCallback: (callback: (previousValue: T | NoValue) => T | NoValue) => void +} + +// useFacetState returns this setter type +type Setter = (value: V | ((previousValue: V | NoValue) => V | NoValue)) => void +``` + +### Key Characteristics + +- **Observable**: Components subscribe to facets and update when values change +- **Composable**: Facets can be derived from other facets using transformation functions +- **Performant**: Updates bypass React reconciliation when used with `fast-*` components +- **Type-safe**: Full TypeScript support with type inference + +--- + +## Core Packages + +### `@react-facet/core` + +Core facet data structure, hooks, and utilities. + +**Key Exports:** + +- **Hooks**: `useFacetState`, `useFacetMap`, `useFacetEffect`, `useFacetCallback`, `useFacetMemo`, `useFacetWrap`, `useFacetWrapMemo`, `useFacetUnwrap`, `useFacetTransition` +- **Components**: `Map`, `Mount`, `With` +- **Factories**: `createFacet`, `createStaticFacet`, `createReadOnlyFacet`, `createFacetContext` +- **Utilities**: `batch`, `NO_VALUE`, equality checks, `startFacetTransition` + +### `@react-facet/dom-fiber` + +Custom React renderer that natively understands facets. + +**Key Features:** + +- Drop-in replacement for `react-dom` +- Provides `fast-*` components (e.g., `fast-div`, `fast-text`, `fast-input`) +- Direct facet binding to DOM without reconciliation +- Optimized for Coherent Gameface (special numeric CSS properties) + +### `@react-facet/shared-facet` + +Interface layer for game engine communication (Gameface integration). + +--- + +## Facet Patterns & Conventions + +### 1. Creating Facets + +**Use `useFacetState` for local component state:** + +```typescript +const [counterFacet, setCounter] = useFacetState(0) +``` + +**Important:** The facet returned by `useFacetState` maintains a **stable reference** across all re-renders. Unlike `useFacetMap` or `useFacetWrap`, the facet instance never changesβ€”only its internal value updates when you call the setter. + +:::warning Critical: Setter Callbacks Receive Option +**When using the functional form of the setter**, the previous value parameter is `Option` (`T | NO_VALUE`), not just `T`. You must check for `NO_VALUE` before using the value: + +```typescript +const [itemsFacet, setItems] = useFacetState([]) + +// ❌ WRONG - current might be NO_VALUE (a Symbol), can't spread! +setItems((current) => [...current, newItem]) + +// βœ… CORRECT - Check for NO_VALUE first +setItems((current) => (current !== NO_VALUE ? [...current, newItem] : [newItem])) +``` + +This is the same as `useFacetUnwrap` - facet values are always `T | NO_VALUE`. +::: + +**NO_VALUE Retention Behavior in Setters:** + +When a setter callback returns `NO_VALUE`, the facet **retains its previous value** rather than updating. The internal state becomes `NO_VALUE`, but listeners are not notified, so subscribers continue seeing the last emitted value. + +```typescript +const [countFacet, setCount] = useFacetState(0) + +// Stop updating once count reaches 5 +setCount((current) => { + if (current === NO_VALUE) return 0 + if (current >= 5) return NO_VALUE // Retains value 5, doesn't notify listeners + return current + 1 +}) + +// countFacet subscribers will see: 0 β†’ 1 β†’ 2 β†’ 3 β†’ 4 β†’ 5 β†’ (stays 5) +``` + +This is useful for: + +- Conditional updates (preventing state changes under certain conditions) +- Validation (rejecting invalid updates while keeping previous valid value) +- Clamping values (stopping updates at a threshold) + +**Use `useFacetWrap` to convert props that may be values or facets:** + +```typescript +// Accepts Facet or T, always returns Facet +const facet = useFacetWrap(maybeFacetProp) +``` + +**Use `useFacetWrapMemo` when you need a stable facet reference:** + +```typescript +// Facet reference stays stable even when the value changes +const stableFacet = useFacetWrapMemo(maybeFacetProp) +``` + +**`useFacetWrap` vs `useFacetWrapMemo` - When to use which:** + +**`useFacetWrap` (creates new facet on value change):** + +- βœ… Default choice for most wrapping scenarios +- βœ… Simpler implementation, lower overhead +- βœ… Best when facet reference changes don't matter +- ⚠️ Creates new facet instance when wrapped value changes + +**`useFacetWrapMemo` (stable facet, updates internal value):** + +- βœ… Maintains stable facet reference +- βœ… Best when wrapping frequently changing props +- βœ… Prevents downstream re-renders from facet reference changes +- ⚠️ Slightly higher overhead (uses effects for synchronization) + +**Example:** + +```typescript +// useFacetWrap: new facet instance on each prop change +const wrappedFacet = useFacetWrap(propValue) + +// useFacetWrapMemo: same facet instance, value updates internally +const memoizedFacet = useFacetWrapMemo(propValue) +``` + +**Use shared facets for data from external sources:** + +Most facet values should come from a shared facet data source (e.g., via context or imported modules), not created locally. + +**Use `createFacet` only for testing or very advanced scenarios:** + +```typescript +// Primarily for testing - creating mock data +const mockFacet = createFacet({ + initialValue: 'test-value', + startSubscription: (update) => { + // Custom subscription logic + return () => {} // cleanup + }, + equalityCheck: defaultEqualityCheck, +}) +``` + +> **Note**: `createFacet` is a low-level API not intended for typical application code. Prefer `useFacetState` or `useFacetWrap` in components, or use shared facet sources. + +### 2. Deriving Facets (Composition) + +**Use `useFacetMap` for lightweight derived facets:** + +```typescript +const healthBarClass = useFacetMap( + (player) => (player.health > 50 ? 'healthy' : 'low-health'), + [], // non-facet dependencies (props, local variables) + [playerFacet], // facet dependencies +) +``` + +**NO_VALUE Retention Behavior:** + +When a mapping function (in `useFacetMap` or `useFacetMemo`) returns `NO_VALUE`, the derived facet **retains its previous value** rather than updating. The observer does not notify listeners, so subscribers continue seeing the last successfully mapped value. + +```typescript +const [countFacet, setCount] = useFacetState(0) + +// Once count reaches 5, the mapped facet stops updating and retains the value 4 +const clampedFacet = useFacetMap((count) => (count < 5 ? count : NO_VALUE), [], [countFacet]) + +// clampedFacet will show: 0, 1, 2, 3, 4, 4, 4, 4... (stuck at 4) +// Even though countFacet continues: 0, 1, 2, 3, 4, 5, 6, 7... +``` + +This is useful for: + +- Conditional updates (only propagating values that meet certain criteria) +- Filtering unwanted values +- Clamping values at boundaries + +**Map multiple facets:** + +```typescript +const fullName = useFacetMap( + (firstName, lastName) => `${firstName} ${lastName}`, + [], // non-facet dependencies + [firstNameFacet, lastNameFacet], // facet dependencies +) +``` + +**Use `useFacetMemo` for cached/expensive derived facets:** + +```typescript +// Use when the derived facet has many subscribers or expensive computation +const expensiveResult = useFacetMemo((data) => heavyComputation(data), [], [dataFacet]) +``` + +**Understanding the two dependency arrays:** + +All facet hooks use a dual dependency array pattern: + +1. **First array** (`deps`): Non-facet dependencies (props, local variables, functions) - works like standard React hooks +2. **Second array** (`facets`): Facet dependencies - these don't change reference (which is how facets maintain performance), so we need a separate mechanism to monitor their value changes + +```typescript +const localMultiplier = props.multiplier + +const result = useFacetMap( + (value) => value * localMultiplier, + [localMultiplier], // ← Non-facet dependencies + [valueFacet], // ← Facet dependencies +) +``` + +**`useFacetMap` vs `useFacetMemo` - When to use which:** + +Both hooks derive values from facets with identical APIs, but have different performance characteristics: + +**Important: Facet Reference Stability** + +Both `useFacetMap` and `useFacetMemo` **create a new facet reference when any dependency changes**: + +- Changes to the `dependencies` array (non-facet dependencies) β†’ new facet reference +- Changes to the `facets` array (different facet instances) β†’ new facet reference +- Changes to the `equalityCheck` function β†’ new facet reference + +This is expected behavior and mirrors how React's `useMemo` works. The returned facet reference is memoized on these dependencies. + +**`useFacetMap` (lightweight):** + +- βœ… Fast to initialize (no internal facet creation overhead) +- βœ… Best for simple transformations (property access, string concatenation) +- βœ… Best for few subscribers (1-2 components) +- ⚠️ Computation runs independently for each subscriber +- **Use as default** for most derivations + +**`useFacetMemo` (cached):** + +- ⚠️ Heavier to initialize (uses `createFacet` internally) +- βœ… Caches results across all subscribers (single computation) +- βœ… Best for expensive computations +- βœ… Best for many subscribers (3+ components) +- **Use when profiling shows** a performance bottleneck with `useFacetMap` + +**Example comparison:** + +```typescript +// If this derived facet is used by 5 components: + +// useFacetMap: computation runs 5 times (once per subscriber) +const lightweightFacet = useFacetMap(expensiveFunc, [], [sourceFacet]) + +// useFacetMemo: computation runs once, result cached for all 5 subscribers +const cachedFacet = useFacetMemo(expensiveFunc, [], [sourceFacet]) +``` + +**In practice:** Start with `useFacetMap` for all derivations. Switch to `useFacetMemo` only when: + +1. You identify a performance issue (profiling shows repeated expensive computations) +2. You know the facet will have many subscribers (e.g., passed to a list of components) +3. The mapping function is clearly expensive (complex calculations, large data processing) + +### 3. Binding Facets to UI + +**Use `fast-*` components (requires `@react-facet/dom-fiber`):** + +```typescript +// Direct facet binding - NO reconciliation + + + + +``` + +**Mix facets and static values:** + +```typescript + +``` + +### 4. Side Effects + +**Use `useFacetEffect` for facet-based effects:** + +```typescript +useFacetEffect( + (playerHealth) => { + if (playerHealth < 20) { + playWarningSound() + } + }, + [], // dependencies + [playerHealthFacet], +) +``` + +**Use `useFacetLayoutEffect` for synchronous effects (like useLayoutEffect):** + +```typescript +useFacetLayoutEffect( + (dimensions) => { + measureAndUpdateLayout(dimensions) + }, + [], + [dimensionsFacet], +) +``` + +### 5. Callbacks with Facets + +**Use `useFacetCallback` to create callbacks that depend on facet values:** + +```typescript +const handleSubmit = useFacetCallback( + (username, password) => () => { + submitLoginForm(username, password) + }, + [], + [usernameFacet, passwordFacet], +) +``` + +**When NOT to use `useFacetCallback`:** + +If you only need to update a facet's state, use a regular callback instead. The setter from `useFacetState` gives you access to the current value: + +```typescript +const [itemsFacet, setItems] = useFacetState([]) + +// ❌ Unnecessary - useFacetCallback not needed here +const addItem = useFacetCallback( + (items) => (newItem: string) => { + setItems([...items, newItem]) + }, + [], + [itemsFacet], +) + +// βœ… Better - regular callback with setter's callback form +const addItem = (newItem: string) => { + setItems((current) => (current !== NO_VALUE ? [...current, newItem] : [newItem])) +} +``` + +**When to use `useFacetCallback`:** + +- You need to **read** facet values to use in the callback logic (not just update them) +- The callback needs to stay stable but depend on multiple facet values +- You're passing the callback to child components and want to avoid re-renders + +**When to use regular callbacks:** + +- You only need to **update** facet state (use the setter's callback form) +- You need to read props/local state (use regular `useCallback`) +- Simple event handlers that don't depend on facet values + +### 5b. Advanced State Management + +**Note:** `useFacetReducer` and `useFacetPropSetter` are **not recommended** for general use. They are underused in practice and may be removed in the future. Prefer `useFacetState` with the setter's callback form instead. + +**`useFacetReducer` - NOT RECOMMENDED:** + +React Facet provides a parallel to React's `useReducer`, but returns a facet as the value. However, this hook is rarely needed in practice. + +```typescript +// ⚠️ NOT RECOMMENDED - Use useFacetState instead +type State = { count: number } +type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' } + +const reducer = (state: Option, action: Action): Option => { + if (state === NO_VALUE) return { count: 0 } + + switch (action.type) { + case 'increment': + return { count: state.count + 1 } + case 'decrement': + return { count: state.count - 1 } + case 'reset': + return { count: 0 } + } +} + +const [stateFacet, dispatch] = useFacetReducer(reducer, { count: 0 }) + +// Usage +dispatch({ type: 'increment' }) +``` + +**`useFacetPropSetter` - NOT RECOMMENDED:** + +Returns a setter function for a specific property of a facet object. In practice, using the setter's callback form is more straightforward. + +```typescript +// ⚠️ NOT RECOMMENDED - Use setter callback form instead +type FormData = { + username: string + email: string +} + +const [formFacet, setForm] = useFacetState({ + username: '', + email: '', +}) + +// Create setters for individual properties +const setUsername = useFacetPropSetter(formFacet, 'username') +const setEmail = useFacetPropSetter(formFacet, 'email') + +// Use in components + setUsername(e.target.value)} /> + setEmail(e.target.value)} /> + +// βœ… BETTER - Use setter callback form directly + setForm(current => + current !== NO_VALUE ? { ...current, username: e.target.value } : { username: e.target.value, email: '' } +)} /> +``` + +### 6. Conditional Rendering + +**CRITICAL: Always use `Mount` for conditional rendering, never `useFacetUnwrap`:** + +```typescript +// ❌ WRONG - Causes re-renders, defeats facet purpose! +const isVisible = useFacetUnwrap(isVisibleFacet) +if (isVisible !== NO_VALUE && !isVisible) return null +return + +// βœ… CORRECT - Use Mount component + + + +``` + +**Use `Mount` component for conditional mounting:** + +```typescript + + + +``` + +**Use `Map` component for lists:** + +```typescript +;{(itemFacet, index) => } + +// In a separate component to follow Rules of Hooks +const ItemRow = ({ itemFacet }: { itemFacet: Facet }) => { + const nameFacet = useFacetMap((item) => item.name, [], [itemFacet]) + return ( +
+ +
+ ) +} +``` + +**Use `Unwrap` component to extract plain values with controlled re-render scope:** + +The `Unwrap` component uses `useFacetUnwrap` internally but confines re-renders to its children instead of the entire component. Use it for: + +- Interfacing with third-party components that accept plain values +- Multi-branch conditional rendering (better than multiple `Mount` components) +- Limiting re-render scope when unwrapping is necessary + +```typescript +// Basic usage - passes plain value to children +{(name) =>
Hello, {name}!
}
+ +// Multi-branch conditional - better than two opposing Mount components +{(cond) => (cond ? : )} + +// Third-party component integration +{(value) => } +``` + +**Key characteristics:** + +- Uses `useFacetUnwrap` internally (causes re-renders on value changes) +- Re-render scope is limited to children, not entire parent component +- Handles `NO_VALUE` automatically (returns `null` if facet has no value) +- Better than multiple `Mount` components for mutually-exclusive branches + +**When to use:** + +- βœ… Interfacing with third-party components +- βœ… Multi-branch conditionals (prefer over multiple `Mount`s) +- βœ… When you need plain values but want controlled re-render scope + +**When NOT to use:** + +- ❌ For binding to DOM properties (use `fast-*` components instead) +- ❌ For simple boolean mounting (use `Mount` instead) +- ❌ When you can keep values as facets (maintain facet semantics) + +**Use `Times` component to repeat UI a dynamic number of times:** + +The `Times` component renders children a specified number of times based on a numeric facet. + +```typescript +// Basic usage +; + {(index, total) => ( +
+ Row {index} of {total} +
+ )} +
+ +// Dynamic count +const [countFacet, setCount] = useFacetState(3) +return ( +
+ {(index) =>
Item {index}
}
+ +
+) +``` + +**Key characteristics:** + +- Uses `Unwrap` internally (re-renders when count changes) +- Children function receives `index` (0-based) and `count` as plain values +- Mounts/unmounts children when count changes (can be expensive) + +**When to use:** + +- βœ… Repeating UI a variable number of times +- βœ… Simple numeric iteration with dynamic count +- βœ… When you don't have array data (just need N repetitions) + +**When NOT to use:** + +- ❌ For rendering lists from array data (use `Map` instead) +- ❌ When you need per-item facets (use `Map` with array facet) +- ❌ For static repetition (use regular array mapping) + +**See also:** + +- Public documentation: `docs/docs/api/mount-components.md#unwrap` +- Public documentation: `docs/docs/api/mount-components.md#times` + +### 7. Performance Optimization with Transitions + +**Use `useFacetTransition` for heavy updates in components:** + +React Facet supports React 18's concurrent features through `useFacetTransition` and `startFacetTransition`. These mark facet updates as low-priority transitions, keeping the UI responsive during expensive operations. + +**Key characteristics:** + +- **Stable callbacks** - The `startTransition` function from `useFacetTransition` is stable and doesn't need to be in dependency arrays +- **Batching** - Uses `batchTransition` internally with separate task queues for transition vs non-transition updates +- **Error handling** - Errors cancel remaining queued tasks and re-throw +- **Nesting support** - Transitions can be nested; inner transitions complete when outer ones do + +```typescript +const [isPending, startTransition] = useFacetTransition() + +const handleHeavyUpdate = () => { + // High-priority update - runs immediately + setInputFacet(newValue) + + // Low-priority update - can be interrupted + startTransition(() => { + try { + const results = expensiveComputation(newValue) + setResultsFacet(results) + } catch (error) { + setErrorFacet(error) + } + }) +} + +// Show pending state +return ( +
+ {isPending &&
Processing...
} + +
+) +``` + +**Use `startFacetTransition` outside components:** + +```typescript +// In utility functions or event handlers +export const loadDataAsTransition = (setData: (data: string[]) => void, newData: string[]) => { + startFacetTransition(() => { + // Heavy update marked as low priority + setData(newData) + }) +} + +// Use in component +const Component = () => { + const [dataFacet, setData] = useFacetState([]) + + const handleLoad = () => { + const newData = generateData() + loadDataAsTransition(setData, newData) + } + + return +} +``` + +**When to use transitions:** + +- βœ… Heavy facet updates that affect many components +- βœ… Expensive computations triggered by facet changes +- βœ… Large list updates or complex UI changes +- βœ… Keeping input fields responsive during processing +- βœ… Shared state updates where `isPending` isn't needed (use `startFacetTransition`) +- ❌ Don't use for urgent UI feedback (like input values) +- ❌ Don't use for critical user interactions + +**Best practices:** + +- Always wrap risky computations in try-catch blocks within transitions +- Use `startFacetTransition` for shared state to avoid provider re-renders +- The `startTransition` callback is stable - don't include it in dependency arrays +- Transitions can be nested for complex update patterns + +### 8. Unwrapping (Use Sparingly!) + +**Use `useFacetUnwrap` only when absolutely necessary:** + +```typescript +// ⚠️ WARNING: Creates real component state - causes re-renders! +const plainValue = useFacetUnwrap(someFacet) +``` + +:::danger Critical: Always Check for NO_VALUE +**`useFacetUnwrap` returns `T | NO_VALUE`, not just `T`!** You must always check for `NO_VALUE` before using the unwrapped value, otherwise you'll get TypeScript errors. + +```typescript +const value = useFacetUnwrap(numberFacet) + +// ❌ WRONG - TypeScript error! value might be NO_VALUE +if (value > 50) { ... } + +// βœ… CORRECT - Check for NO_VALUE first +if (value !== NO_VALUE && value > 50) { ... } +``` + +::: + +**When to use `useFacetUnwrap`:** + +1. **Passing to non-facet-aware components** (though refactoring the component to accept facets is preferred): + + ```typescript + const value = useFacetUnwrap(facet) + + // Always check for NO_VALUE before using + if (value === NO_VALUE) return null + + return + ``` + +2. **Conditional mounting** - **DON'T DO THIS!** Use `Mount` or `With` components instead: + + ```typescript + // ❌ WRONG - causes component re-render, defeats facet purpose + const isVisible = useFacetUnwrap(isVisibleFacet) + if (isVisible !== NO_VALUE && !isVisible) return null + + // βœ… CORRECT - Use Mount component, no re-renders + + + + ``` + +**Common NO_VALUE handling patterns:** + +```typescript +// Early return +const value = useFacetUnwrap(facet) +if (value === NO_VALUE) return null + +// Default value +const value = useFacetUnwrap(facet) +const safeValue = value === NO_VALUE ? defaultValue : value + +// Guard in JSX +const items = useFacetUnwrap(arrayFacet) +return items !== NO_VALUE && items.map(...) +``` + +**Performance Impact:** + +`useFacetUnwrap` defeats the primary benefit of facets by triggering React re-renders. Use it as a last resort, not as a standard pattern. + +### 9. Context + +**Use `createFacetContext` for facet-based context:** + +```typescript +const PlayerContext = createFacetContext() + +// Provider + + + + +// Consumer +const playerFacet = useContext(PlayerContext) +``` + +--- + +## Important Conventions + +### Naming + +- Facet variables: suffix with `Facet` (e.g., `counterFacet`, `playerHealthFacet`) +- Facet setters: prefix with `set` (e.g., `setCounter`, `setPlayerHealth`) +- Derived facets: descriptive names indicating transformation (e.g., `healthBarClass`, `formattedDate`) + +### Dual Dependencies Arrays + +All facet hooks use **two dependency arrays** (unlike standard React hooks): + +```typescript +useFacetMap( + (value) => transform(value, localVar), + [localVar], // First array: non-facet dependencies (props, local vars) + [facet], // Second array: facet dependencies +) +``` + +**Why two arrays?** + +Facets don't change reference (this is key to their performance), so we need a separate mechanism to track when their _values_ change. The second array tells the hook which facets to subscribe to for value changes. + +**Examples:** + +```typescript +// Local variable dependency +const multiplier = props.multiplier +useFacetMap((x) => x * multiplier, [multiplier], [xFacet]) + +// Multiple facets, no local dependencies +useFacetMap((a, b) => a + b, [], [aFacet, bFacet]) + +// Both types of dependencies +const prefix = props.prefix +useFacetMap((name) => `${prefix}: ${name}`, [prefix], [nameFacet]) +``` + +### Equality Checks + +Use equality checks to prevent unnecessary updates: + +```typescript +import { shallowObjectEqualityCheck, shallowArrayEqualityCheck, strictEqualityCheck } from '@react-facet/core' + +// For objects +useFacetMap((a, b) => ({ a, b }), [], [facetA, facetB], shallowObjectEqualityCheck) + +// For arrays +useFacetMap((a, b) => [a, b], [], [facetA, facetB], shallowArrayEqualityCheck) + +// For primitives (optional - default is already optimized) +useFacetMap((x) => x * 2, [], [xFacet], strictEqualityCheck) + +// No check needed for primitives - defaultEqualityCheck is used automatically and is fastest +useFacetMap((x) => x * 2, [], [xFacet]) // Optimized by default +``` + +**Key differences between equality checks:** + +- **`defaultEqualityCheck`** (used automatically when omitted): + + - **Performance optimized** - Inlined in mapping functions for best speed + - For primitives: uses `===` comparison + - For objects/arrays: always returns `false` (treats as mutable/always different) + - Accepts any type but not type-safe + +- **`strictEqualityCheck`**: + + - **Type-safe** - TypeScript constraint: `` + - Only works with primitives (`boolean | number | string | undefined | null`) and functions + - TypeScript will prevent usage with objects/arrays + - Uses `===` comparison + - Slightly slower than `defaultEqualityCheck` (not inlined) + +- **`shallowObjectEqualityCheck`**: For objects with primitive values (deep value comparison) + +- **`shallowArrayEqualityCheck`**: For arrays of primitives (deep value comparison) + +**Best practices:** + +- For primitives: Omit the equality check (uses optimized `defaultEqualityCheck`) +- For objects: Always use `shallowObjectEqualityCheck` or similar +- For arrays: Always use `shallowArrayEqualityCheck` or similar +- For type safety: Use `strictEqualityCheck` (though it's slightly slower) +- For functions: Use `strictEqualityCheck` (function reference comparison) + +### NO_VALUE + +`NO_VALUE` is a special sentinel value representing an uninitialized facet. It's a unique symbol that must be checked explicitly. + +**CRITICAL with `useFacetUnwrap`:** + +```typescript +import { NO_VALUE, useFacetUnwrap } from '@react-facet/core' + +// useFacetUnwrap returns T | NO_VALUE +const value = useFacetUnwrap(numberFacet) + +// ❌ WRONG - TypeScript error! value might be NO_VALUE +const doubled = value * 2 + +// ❌ WRONG - TypeScript error! NO_VALUE is not a number +if (value > 50) { ... } + +// βœ… CORRECT - Check for NO_VALUE first +if (value !== NO_VALUE) { + const doubled = value * 2 // Now TypeScript knows value is number + if (value > 50) { ... } +} +``` + +**Other uses:** + +```typescript +// ⚠️ Avoid in application code - use useFacetCallback instead +// Direct facet.get() is primarily for testing scenarios +const value = someFacet.get() +if (value === NO_VALUE) { + // Handle uninitialized state +} + +// βœ… In application code, use facet hooks instead +const handleClick = useFacetCallback( + (value) => () => { + if (value > 50) { + // Use value here + } + }, + [], + [someFacet], +) + +// In useFacetMap (facet mapping handles NO_VALUE automatically) +const mappedFacet = useFacetMap( + (value) => { + // value here is T, not T | NO_VALUE + // useFacetMap only calls this when value is available + return value * 2 + }, + [], + [numberFacet], +) +``` + +**Key difference:** + +- **`useFacetMap`**: Automatically waits for all facets to have values, callback receives `T` +- **`useFacetUnwrap`**: Returns `T | NO_VALUE` immediately, YOU must check for `NO_VALUE` + +### Batching + +Batching is built into the library by default and **not intended for public use**. + +The `batch` function exists primarily for internal library use and is exported only for library internals. In normal application code: + +- **Don't use `batch` directly** - It's marked as `@private` in the source code +- **For transitions, use the public APIs**: `useFacetTransition` or `startFacetTransition` +- **The library handles batching automatically** - Facet updates are batched internally + +**Internal implementation detail:** + +- `batch` - General-purpose batching for facet updates +- `batchTransition` - Special batching for transitions (used by `useFacetTransition`/`startFacetTransition`) +- Separate task queues for transition vs non-transition updates ensure proper priority ordering + +--- + +## fast-\* Components + +### When to Use fast-\* Components + +**Use `fast-*` components when you need to bind a Facet to a DOM property:** + +```typescript +// βœ… Use fast-div when binding facet values + + + + +// βœ… Use fast-input when value is a facet + +``` + +**Use regular HTML elements when working with static values or non-facet props:** + +```typescript +// βœ… Regular HTML is fine for static content +
+

Static text content

+ +
+ +// βœ… Mix regular HTML with fast-* when needed +
+ + +
+``` + +**Key principle**: `fast-*` components bypass React reconciliation for property updates. Use them when you need this performance benefit (binding facets), but regular HTML is perfectly fine otherwise. + +### Available Components + +**Primary Components:** + +- **`fast-div`** - Container element that accepts facet props +- **`fast-text`** - Text content from a facet (renders as text node, no wrapper element) +- **`fast-input`** - Text input that can bind to facet values +- **`fast-textarea`** - Multi-line text input with facet support +- **`fast-img`** - Images with facet-bindable src and other attributes +- **`fast-span`** - Inline element with facet support +- **`fast-p`** - Paragraph element with facet support +- **`fast-a`** - Anchor/link element with facet support + +### Usage Examples + +```typescript +// fast-* components accept Facet or T for all props + + + + +// Regular HTML for static structure +
+
+

Player Stats

+
+
+ {/* Use fast-text only where facet binding is needed */} + Health: +
+
+``` + +### Gameface Optimizations + +`fast-*` components support numeric CSS properties (faster than strings): + +- Properties ending in `PX`, `VH`, `VW` (e.g., `widthPX`, `heightVH`) +- Avoids string construction/parsing overhead + +--- + +## Code Generation Guidelines + +### When to Use Facets + +βœ… **Use facets when:** + +- Frequent updates to UI (animations, counters, health bars) +- Derived state from multiple sources +- Performance-critical game UI +- Working within `@react-facet/dom-fiber` renderer + +❌ **Don't use facets when:** + +- One-time static values +- Infrequent updates where re-renders are acceptable +- Working with third-party components expecting plain props + +### Facet Creation Patterns + +```typescript +// βœ… Good - use hooks in components for local state +const [stateFacet, setState] = useFacetState(initialValue) + +// βœ… Good - derive from other facets +const derivedFacet = useFacetMap(fn, deps, [sourceFacet]) + +// βœ… Good - wrap props that might be values or facets +const wrappedFacet = useFacetWrap(propValue) + +// βœ… Good - use shared facets from context/modules +const sharedFacet = useContext(DataContext) + +// ❌ Avoid - createFacet in application code +const facet = createFacet({ initialValue, startSubscription }) + +// βœ… OK - createFacet for testing only +const mockFacet = createFacet({ initialValue: 'test' }) +``` + +### Keep Facets at Appropriate Scope + +```typescript +// βœ… Good - facet at component level +function PlayerHealth() { + const [healthFacet, setHealth] = useFacetState(100) + return +} + +// βœ… Good - shared facet via context +const PlayerContext = createFacetContext() + +// ❌ Avoid - recreating facets unnecessarily +function Bad() { + const healthFacet = useFacetMap((p) => p.health, [], [playerFacet]) + const healthFacet2 = useFacetMap((p) => p.health, [], [playerFacet]) // duplicate! +} +``` + +### Type Safety + +**Prefer `type` over `interface`:** + +```typescript +// βœ… Good - use type for facet data structures +type PlayerData = { + health: number + mana: number + name: string +} + +const [playerFacet, setPlayer] = useFacetState({ + health: 100, + mana: 50, + name: 'Steve', +}) + +// Type inference works for simple cases +const [counterFacet, setCounter] = useFacetState(0) // inferred as number +``` + +--- + +## Common Pitfalls + +### 1. Mixing Facets and React State + +❌ **Don't mix paradigms unnecessarily:** + +```typescript +const [count, setCount] = useState(0) // React state +const [countFacet, setCountFacet] = useFacetState(0) // Facet state +// Choose one approach per component +``` + +### 2. Forgetting Dependencies (Especially Non-Facet Dependencies) + +```typescript +// ❌ Missing local variable in FIRST dependency array +const multiplier = props.multiplier +const result = useFacetMap( + (value) => value * multiplier, + [], // ❌ Missing: [multiplier] + [valueFacet], +) + +// βœ… Include all non-facet dependencies in first array +const result = useFacetMap( + (value) => value * multiplier, + [multiplier], // βœ… Non-facet dependencies here + [valueFacet], // βœ… Facet dependencies here +) +``` + +### 3. Overusing useFacetUnwrap (Performance Killer!) + +```typescript +// ❌ Causes re-renders - defeats the entire purpose of facets! +const value = useFacetUnwrap(facet) +return
{value}
+ +// βœ… Use fast-text with facet - no re-renders +return + +// ❌ Unwrapping for conditional rendering +const isVisible = useFacetUnwrap(isVisibleFacet) +if (!isVisible) return null +return + +// βœ… Use Mount component - scopes re-render +return ( + + + +) +``` + +**Remember**: `useFacetUnwrap` creates real component state and triggers re-renders. Use it only as a last resort when interfacing with non-facet-aware code. + +### 4. Forgetting to Check for NO_VALUE After useFacetUnwrap + +```typescript +// ❌ TypeScript ERROR - value might be NO_VALUE! +const value = useFacetUnwrap(numberFacet) +const doubled = value * 2 // Error: NO_VALUE is not a number + +// ❌ TypeScript ERROR - can't compare NO_VALUE with number +if (value > 50) { ... } + +// βœ… Always check for NO_VALUE first +const value = useFacetUnwrap(numberFacet) +if (value !== NO_VALUE) { + const doubled = value * 2 // βœ“ Now TypeScript knows it's a number + if (value > 50) { ... } // βœ“ Safe to compare +} + +// βœ… Or use early return pattern +const value = useFacetUnwrap(numberFacet) +if (value === NO_VALUE) return null +// After this point, TypeScript knows value is the actual type +``` + +### 5. Forgetting to Check for NO_VALUE in useFacetState Setter Callbacks + +```typescript +const [itemsFacet, setItems] = useFacetState([]) + +// ❌ WRONG - current might be NO_VALUE (a Symbol), can't spread! +setItems((current) => [...current, newItem]) + +// ❌ WRONG - NO_VALUE doesn't have .filter method +setItems((current) => current.filter((item) => item !== oldItem)) + +// βœ… CORRECT - Check for NO_VALUE first +setItems((current) => (current !== NO_VALUE ? [...current, newItem] : [newItem])) + +// βœ… CORRECT - Handle NO_VALUE in all operations +setItems((current) => (current !== NO_VALUE ? current.filter((item) => item !== oldItem) : [])) +``` + +**Critical**: The setter callback receives `Option` (i.e., `T | NO_VALUE`), just like `useFacetUnwrap`. Always check before using the value! + +### 6. Not Using Equality Checks for Objects/Arrays + +```typescript +// ❌ Will update on every check (reference equality) +const combined = useFacetMap((a, b) => ({ a, b }), [], [facetA, facetB]) + +// βœ… Use appropriate equality check +const combined = useFacetMap((a, b) => ({ a, b }), [], [facetA, facetB], shallowObjectEqualityCheck) +``` + +### 7. Calling Hooks Inside Conditionals, Loops, or Nested Functions + +```typescript +// ❌ WRONG - Hook inside Map callback (nested function) + + {(itemFacet, index) => ( +
+ item.name, [], [itemFacet])} /> +
+ )} +
+ +// ❌ WRONG - Hook inside conditional +{Math.random() > 0.5 ? useFacetMap(...) : useFacetMap(...)} + +// ❌ WRONG - Hook inside loop +{items.map(item => useFacetMap(...))} + +// βœ… CORRECT - Hook at top level (before return) +const nameFacet = useFacetMap((item) => item.name, [], [itemFacet]) +return + +// βœ… ALSO CORRECT - Hook in JSX during render (not in conditional/loop/function) +return item.name, [], [itemFacet])} /> + +// βœ… BEST PRACTICE - Separate component with hooks at top level +const ItemRow = ({ itemFacet }: { itemFacet: Facet }) => { + const nameFacet = useFacetMap((item) => item.name, [], [itemFacet]) + return +} +``` + +**Rules of Hooks**: Hooks can be called at the component's top level OR directly in JSX during render, but **never** inside conditionals, loops, or nested functions (like Map callbacks). Best practice is to define derived facets at the top level for clarity and to avoid recreating them on each render. + +### 8. Using facet.get() in Application Code + +```typescript +// ❌ WRONG - .get() breaks reactivity, causes stale closures +const addItem = () => { + const name = newItemNameFacet.get() + if (name === NO_VALUE || name.trim() === '') return + // Process name... +} + +const selectItem = (id: string) => { + const currentSelected = selectedIdFacet.get() + setSelectedId(currentSelected === id ? null : id) +} + +// βœ… CORRECT - Use useFacetCallback to access facet values reactively +const addItem = useFacetCallback( + (name) => () => { + if (name.trim() === '') return + // Process name... + }, + [], + [newItemNameFacet], +) + +const selectItem = useFacetCallback( + (currentSelected) => (id: string) => { + setSelectedId(currentSelected === id ? null : id) + }, + [], + [selectedIdFacet], +) +``` + +**Critical**: `facet.get()` is a low-level API intended for **testing and internal library use only**. In application code: + +- ❌ **Don't use** `.get()` in event handlers or regular functions +- βœ… **Do use** `useFacetCallback` to access facet values in callbacks +- βœ… **Do use** `useFacetMap` to derive new facets from existing ones +- βœ… **Only exception**: `.get()` is acceptable in test files for asserting values + +**Why this matters**: Using `.get()` breaks the reactive chain. The value is read once and can become stale. Using `useFacetCallback` ensures the callback always has the latest facet values. + +### 9. Using fast-\* Components for Static Content + +```typescript +// ❌ WRONG - fast-div used when className is a static string + + + + + + +// ❌ WRONG - fast-span used with no facet bindings +Static label text + +// βœ… CORRECT - Regular HTML for static attributes, fast-text only for facet +
+
+ +
+
+ +// βœ… CORRECT - Regular span for static content +Static label text + +// βœ… CORRECT - Use fast-div ONLY when binding a facet to an attribute + + + +``` + +**Rule**: Use `fast-*` components **only** when you need to bind a facet value to a DOM attribute. For static content, regular HTML elements are simpler, more idiomatic, and perfectly fine. + +**When to use each:** + +- `fast-div` β†’ when `className`, `style`, or other attributes are facets +- `
` β†’ when all attributes are static strings +- `fast-text` β†’ when text content is a facet +- Text nodes β†’ when text content is static +- `fast-input` β†’ when `value` or other attributes are facets +- `` β†’ NEVER (use `fast-input` instead, see next pitfall) + +### 10. Unwrapping Facets for Form Inputs When fast-\* Alternatives Exist + +```typescript +// ❌ WRONG - Unwrapping causes component re-renders! +const username = useFacetUnwrap(usernameFacet) +const sortBy = useFacetUnwrap(sortByFacet) + +return ( + <> + setUsername(e.target.value)} /> + setSortBy(e.target.value)} /> + +) +``` + +**Critical Performance Pattern**: Form inputs are a common place where developers unnecessarily use `useFacetUnwrap`, causing re-renders. Always prefer `fast-input`, `fast-textarea`, or other facet-aware form components. + +**Available facet-aware form components:** + +- `fast-input` - Text input (single-line) +- `fast-textarea` - Text input (multi-line) +- Check `@react-facet/dom-fiber` for other form components + +**When unwrapping IS necessary:** + +- Native ` updateUsername(e.target.value)} placeholder="Enter username" /> +
+ +
+ + updateEmail(e.target.value)} placeholder="Enter email" /> +
+ + {/* Submit button with validation - unwrap facet for non-facet-aware button */} + + + {/* Results rendered with facet */} + +
+ + {(resultFacet, index) => ( +
+ +
+ )} +
+
+
+ + ) +} + +// Simulated expensive function +function heavyDataProcessing(data: FormData): string[] { + // Expensive computation here + return [`Processed user: ${data.username}`, `Email verified: ${data.email}`] +} +``` + +--- + +## Copilot-Specific Guidance + +### Critical Rules (Must Follow) + +**Before doing anything else, review the "⚠️ Top 3 Critical Errors to Avoid" section at the top of this document.** These are the most common mistakes that completely defeat the purpose of React Facet. + +When generating or modifying React Facet code: + +1. **CRITICAL: Always check for `NO_VALUE` after `useFacetUnwrap`** - the return type is `T | NO_VALUE`, not `T` +2. **CRITICAL: Always check for `NO_VALUE` in `useFacetState` setter callbacks** - the previous value is `Option` (`T | NO_VALUE`), not just `T` +3. **CRITICAL: Avoid `useFacetUnwrap`** unless absolutely necessary (causes re-renders!) +4. **CRITICAL: Use `Mount` for conditional rendering, NEVER `useFacetUnwrap`** - unwrapping defeats the entire purpose of facets +5. **CRITICAL: Use `createRoot` (not `render`) for mounting** - `render` is deprecated +6. **CRITICAL: NEVER use `facet.get()` in application code** - it's for testing only; use `useFacetCallback` instead +7. **CRITICAL: Use `fast-input` instead of unwrapping for ``** - unwrapping causes re-renders +8. **CRITICAL: Use `fast-*` components ONLY when binding facets to attributes** - use regular HTML for static content +9. **Always use facet naming convention** (`*Facet` suffix for variables) +10. **Use `useFacetState` or `useFacetWrap` for creating facets** - avoid `createFacet` in application code +11. **Use TWO dependency arrays correctly**: + +- First array: non-facet dependencies (props, local vars) +- Second array: facet dependencies + +12. **Default to `useFacetMap` for derivations** - only use `useFacetMemo` when you have many subscribers or expensive computations +13. **Use `useFacetWrap` vs `useFacetWrapMemo` appropriately**: + +- `useFacetWrap`: Default choice, creates new facet on value change +- `useFacetWrapMemo`: When you need stable facet references or wrap frequently changing props + +14. **Use transitions for heavy updates**: + +- `useFacetTransition`: In components when you need pending state +- `startFacetTransition`: Outside components or when pending state not needed +- The `startTransition` callback from `useFacetTransition` is stable - don't include it in dependency arrays +- Always wrap risky computations in try-catch blocks within transitions + +15. **Add equality checks** for object/array derivations to prevent unnecessary updates +16. **Handle `NO_VALUE`** in facet operations where appropriate +17. **Understand NO_VALUE retention behavior**: + - When a mapping function (`useFacetMap`/`useFacetMemo`) returns `NO_VALUE`, the derived facet retains its previous value (doesn't notify listeners) + - When a setter callback (`useFacetState`) returns `NO_VALUE`, the facet retains its previous value (doesn't notify listeners) + - Useful for conditional updates, validation, and clamping values +18. **Use `type` instead of `interface`** for TypeScript definitions +19. **Don't use `batch` in application code** - it's for internal library use +20. **Never call hooks inside conditionals, loops, or nested functions** (like Map callbacks) - violates Rules of Hooks +21. **Best practice: Define derived facets at component top level** for clarity and stable references +22. **Test facet-based components** using `@react-facet/dom-fiber-testing-library` +23. **Understand facet reference stability**: + - `useFacetState`: Facet reference is **stable** - never changes across re-renders + - `useFacetMap`/`useFacetMemo`: Create **new facet reference** when dependencies change (non-facet deps, facets array, or equalityCheck) + - `useFacetWrap`: Creates **new facet reference** when wrapped value changes + - `useFacetWrapMemo`: Maintains **stable facet reference**, updates value internally + +When reviewing React Facet code, check for: + +- **Mount for conditional rendering**: NEVER use `useFacetUnwrap` for conditional rendering (CRITICAL) +- **createRoot for mounting**: NEVER use deprecated `render` method (CRITICAL) +- **NO `facet.get()` in application code**: CRITICAL - use `useFacetCallback` instead (CRITICAL) +- **Use `fast-input` for form inputs**: NEVER unwrap facets for `` when `fast-input` exists (CRITICAL) +- **`fast-*` only for facet bindings**: NEVER use `fast-div`, `fast-span`, etc. with static attributes (CRITICAL) +- **Dual dependency arrays**: First for non-facet deps, second for facet deps +- **No missing dependencies** in the first array (props, local variables, functions) +- **Appropriate equality checks** (objects/arrays need custom checks) +- **Correct hook choice for wrapping**: `useFacetWrap` vs `useFacetWrapMemo` based on stability needs +- **Transitions for heavy updates**: Using `useFacetTransition` or `startFacetTransition` for expensive operations +- **Error handling in transitions**: Try-catch blocks wrapping risky computations +- **Minimal use of `useFacetUnwrap`** (red flag if used frequently, especially for ``) +- **`NO_VALUE` checks after `useFacetUnwrap`** (CRITICAL - must check before using unwrapped values) +- **`NO_VALUE` checks in `useFacetState` setter callbacks** (CRITICAL - must check before spreading/accessing properties) +- **NO_VALUE retention awareness**: Understanding that returning `NO_VALUE` from mappers/setters retains previous value +- **Regular HTML for static content**: Use `
`, ``, etc. when no facet bindings needed +- **`createFacet` only in tests** - not in components +- **Hooks not called inside conditionals, loops, or nested functions** (Rules of Hooks) +- **Derived facets defined at top level** for clarity and stability (best practice) +- **`type` over `interface`** in TypeScript definitions +- **`useFacetMap` as default for derivations** - `useFacetMemo` only when needed for performance +- **Stable callback awareness**: Not including `startTransition` from `useFacetTransition` in dependency arrays +- **Facet reference stability awareness**: Understanding when facets create new references vs maintaining stable ones + +--- + +## Maintaining These Instructions + +> **Last Updated**: 27 October 2025 + +To keep these instructions accurate as the project evolves: + +### When to Update This File + +**API Changes** - Update when: + +- New hooks are added to `@react-facet/core` +- Hook signatures change (new parameters, different return types) +- New `fast-*` components are added or deprecated +- New packages are added to the monorepo + +**Pattern Changes** - Update when: + +- Best practices evolve (e.g., new performance patterns discovered) +- Breaking changes require different usage patterns +- New testing utilities are added + +**Structure Changes** - Update when: + +- Folder structure is reorganized +- Import paths change +- New packages or examples are added + +### Verification Checklist + +Periodically verify these sections stay current: + +- [ ] **Hook signatures** match actual implementations in `packages/@react-facet/core/src/hooks/` +- [ ] **Import examples** work with current package structure +- [ ] **fast-\* components list** matches `packages/@react-facet/dom-fiber/src/` +- [ ] **Repository structure** reflects actual folder organization +- [ ] **Code examples** run without errors +- [ ] **Links** to documentation site are valid + +### Integration with Development + +**Release Checklist**: Add to `CONTRIBUTING.md`: + +- [ ] Review and update Copilot instructions for any API changes +- [ ] Verify all code examples in instructions work with new version +- [ ] Update "Last Updated" date in instructions +- [ ] Run `bash scripts/check-copilot-instructions-sync.sh` to verify documentation is in sync + +### Ownership + +**Maintainer Responsibility**: Assign documentation ownership + +- Core team reviews instruction updates in PRs +- Release manager includes docs in release checklist +- Monthly review of instructions for accuracy + +**Documentation Sync Tool**: The repository includes `scripts/check-copilot-instructions-sync.sh` to automatically detect drift between code and documentation. + +--- + +## Documentation Code Examples + +When writing code examples in the documentation (`docs/docs/**/*.md`): + +### Fast-\* Component Usage + +**CRITICAL:** All `fast-*` components (like `fast-text`, `fast-div`, `fast-input`, `fast-span`, `fast-img`, etc.) require importing the renderer: + +```tsx +// βœ… CORRECT - Import renderer for any fast-* components +// @esModuleInterop +import { createRoot } from '@react-facet/dom-fiber' +// ---cut--- +import { useFacetState, useFacetMap } from '@react-facet/core' + +const Example = () => { + const [textFacet] = useFacetState('Hello') + const [classFacet] = useFacetState('my-class') + + return ( + + + + + ) +} +``` + +```tsx +// ❌ WRONG - Missing renderer import causes TypeScript errors +// @esModuleInterop +import { useFacetState } from '@react-facet/core' + +const Example = () => { + const [textFacet] = useFacetState('Hello') + return ( + + {' '} + {/* Error: Property 'fast-div' does not exist */} + {/* Error: Property 'fast-text' does not exist */} + + ) +} +``` + +**The pattern:** + +- Add `// @esModuleInterop` at the top +- Add `import { createRoot } from '@react-facet/dom-fiber'` +- Add `// ---cut---` to hide the import from the rendered example +- Then write your example code with any `fast-*` components + +### Regular HTML Elements + +Regular HTML elements don't need the renderer import: + +```tsx +// βœ… Works without renderer import +// @esModuleInterop +import { useFacetState } from '@react-facet/core' + +const Example = () => { + return
Static content
// Regular HTML - no import needed +} +``` + +### When to Use Each + +- Use any `fast-*` components when demonstrating facet binding to DOM properties +- Use regular HTML for examples focusing on hook usage or component structure +- Never use any `fast-*` component without the renderer import pattern shown above diff --git a/.github/prompts/create-documentation.prompt.md b/.github/prompts/create-documentation.prompt.md new file mode 100644 index 00000000..169d62bb --- /dev/null +++ b/.github/prompts/create-documentation.prompt.md @@ -0,0 +1,453 @@ +--- +description: Create comprehensive public-facing documentation for React Facet APIs (hooks, components, utilities) +--- + +You are helping create **public-facing documentation** for React Facet APIs in the `docs/` directory. + +## Task + +Create thorough, accurate documentation for a specific hook, component, or utility that currently lacks documentation or has incomplete documentation. + +## Process Overview + +This is an **iterative process** - expect multiple rounds of review and refinement. The goal is production-quality documentation that serves as the authoritative reference for users. + +## Steps + +### 1. Identify the Documentation Target + +The user will specify which API to document (e.g., `With` component, `useFacetMap` hook). + +**Initial questions to ask:** + +- What is the exact name of the hook/component/utility? +- Is there existing documentation that needs updating, or is this new? +- Are there related APIs that should be documented at the same time? + +### 2. Assess Existing Documentation Structure + +Before writing anything, understand where this documentation fits: + +**Explore the documentation structure:** + +- Read `docs/docs/api/` to see existing API documentation +- Identify patterns in how hooks vs components are documented +- Note the file naming conventions (kebab-case, organization by type) +- Check `docs/sidebars.js` to understand navigation structure +- Look for related documentation that might need cross-references + +**Key questions to answer:** + +- Where should this documentation file live? (`docs/docs/api/hooks/`, `docs/docs/api/components/`, etc.) +- What filename follows the existing convention? +- How are similar APIs organized and structured? +- What cross-references need to be added (to/from other docs)? +- Does `sidebars.js` need updating to include this page? + +Important: follow existing grouping patterns + +- Many component docs in this repo are collected into shared category pages (for example `mount-components.md` contains `Mount`, `Map`, and `With`) rather than one file per component. Before creating a new `components/` subfolder or a new standalone file, check whether there is an existing category page that the API should be appended to. Only create a new standalone file when the repository already uses individual files for that category (for example a `hooks/` directory with separate files). +- When appending to an existing category page, follow the same ordering, heading styles, and example formatting found on that page. + +**Report your findings:** + +- Proposed file path +- Related documentation files that may need updates +- Sidebar configuration changes needed + +Also include: + +- Whether the API should be appended to an existing category page or created as a new file (and why). +- Exact target file (e.g., `docs/docs/api/mount-components.md` to append to, or `docs/docs/api/components/unwrap.md` to create). Provide rationale matching repository patterns. + +### 3. Deep Research of the API + +Gain a thorough understanding through multiple sources: + +#### Source Code Investigation + +- **Read the implementation file:** + + - Understand every line - what does it actually do? + - Trace through all function calls and dependencies + - Identify type signatures (what's a facet vs regular value?) + - Note any internal implementation details that affect behavior + - Does it use `useFacetUnwrap` (causes re-renders) or maintain facet semantics? + - What are the performance characteristics? + +- **Read JSDoc comments:** + + - Original intent and purpose + - Parameter descriptions + - Return value documentation + +- **Read spec/test files:** + - How is the API actually used? + - What edge cases are tested? + - What scenarios are considered important? + - Extract realistic usage examples + +#### Related Code + +- **Find related APIs:** + + - Similar hooks/components that solve related problems + - APIs that are commonly used together + - Alternatives that users might consider + +- **Search for usage in examples:** + - Check `examples/` directory + - Look for usage patterns in the codebase + - Note common combinations and patterns + +#### Custom Instructions + +- **Review `.github/copilot-instructions.md`:** + - Check for established conventions about this API + - Look for warnings, gotchas, or best practices + - Understand how it fits into the mental model + - Note any critical rules about usage + +**Report your understanding:** + +- What does this API do? (in your own words) +- How does it work internally? +- What are the performance characteristics? +- What are common use cases? +- What are common pitfalls? +- How does it compare to alternatives? + +### 4. Study Documentation Standards + +Before writing, thoroughly review existing documentation to match style and quality: + +**Read 3-5 similar API documentation files:** + +- How are they structured? (sections, ordering) +- What's the typical length and depth? +- How many code examples do they include? +- How do they explain "when to use" vs "when NOT to use"? +- How do they handle performance warnings? +- How do they show type signatures? +- What's the tone and voice? + +**Identify the documentation template/pattern:** + +- Standard sections (Overview, API Reference, Examples, etc.) +- Code example format (live demos, syntax highlighting, etc.) +- Callout patterns (warnings, notes, tips) +- Cross-reference style + +**Report the documentation pattern:** + +- Required sections +- Optional sections +- Example structure +- Callout patterns used + +### 5. Draft the Documentation + +Write comprehensive documentation following the identified patterns: + +#### Required Elements + +**Frontmatter:** + +```markdown +--- +title: [API Name] +sidebar_label: [Short Label] +--- +``` + +**Overview Section:** + +- Clear, concise description of what it does +- Why it exists / what problem it solves +- When to use it vs alternatives + +**API Reference:** + +- Full type signature +- Parameter descriptions with types +- Return value description with type +- Default values +- Optional vs required parameters + +**Usage Examples:** + +- **Minimum 3-4 examples** showing different scenarios: + - Basic usage (simplest case) + - Common real-world usage + - Advanced usage or edge cases + - Usage with related APIs +- Each example should: + - Be complete and runnable + - Include TypeScript types + - Have explanatory comments + - Show realistic variable names + - Follow conventions from custom instructions + +**Performance Considerations:** + +- Does it cause re-renders? When? +- Does it create new facet references? When? +- Are there performance gotchas? +- Comparison with alternatives' performance + +**When to Use / When NOT to Use:** + +- Clear guidance on appropriate use cases +- Explicit anti-patterns or cases to avoid +- Alternatives for cases where this isn't suitable + +**Related APIs:** + +- Links to related hooks/components +- Common combinations +- Alternatives to consider + +**Notes/Warnings:** + +- Edge cases +- Common mistakes +- Critical behavior (like NO_VALUE handling) + +#### Quality Standards + +- **Accuracy**: Every detail must match the actual implementation +- **Completeness**: Cover all parameters, edge cases, and scenarios +- **Clarity**: Explain complex concepts simply +- **Examples**: Show, don't just tell - provide working code +- **Consistency**: Match existing documentation style exactly + +**Present your draft and ask:** + +- Is the explanation clear and accurate? +- Are the examples realistic and helpful? +- Did I miss any important use cases or warnings? +- Does this match the existing documentation style? + +### 6. Handle Cross-References + +Identify and update related documentation: + +**Find documentation that should reference this new page:** + +- Related APIs that might mention this as an alternative +- Tutorial pages that could benefit from this API +- Parent category pages that list APIs + +**Add cross-references:** + +- Link to this new documentation from related pages +- Add to any API listing pages +- Update comparison tables if they exist + +**Update sidebars.js:** + +- Add entry in the appropriate section +- Ensure proper ordering (alphabetical or logical grouping) + +**Report cross-reference updates:** + +- List of files that need new links +- Sidebar configuration changes +- Any navigation structure improvements + +### 7. Iterative Refinement + +This is a collaborative process. After presenting your draft: + +**Expect feedback on:** + +- Technical accuracy ("Actually, it works like this...") +- Example quality ("This example doesn't show the real use case") +- Missing edge cases ("What about when the value is null?") +- Style/tone mismatches ("This doesn't match our voice") +- Structure improvements ("This section should come first") + +**For each round of feedback:** + +- Acknowledge the issue clearly +- Explain how you'll address it +- Make the changes +- Verify the changes against source code +- Present the updated version + +**Continue iterating until:** + +- Technical accuracy is verified +- All use cases are covered +- Examples are realistic and helpful +- Style matches existing documentation +- User confirms it's ready + +### 8. Final Verification + +Before considering the documentation complete: + +**Self-review checklist:** + +- [ ] Read the source code again - does the documentation match? +- [ ] Run through each code example mentally - do they work? +- [ ] Check all type signatures - are they accurate? +- [ ] Verify performance claims - did you trace through the implementation? +- [ ] Review NO_VALUE handling - is it documented where applicable? +- [ ] Check facet vs non-facet values - is it clear throughout? +- [ ] Compare to similar documentation - is quality equivalent? +- [ ] Test all internal links - do they work? +- [ ] Review cross-references - are they bidirectional where appropriate? + +**Ask for final review:** + +- Read through the complete documentation +- Verify against the actual implementation one more time +- Request final approval from the user + +### 9. Summary + +Provide a concise summary: + +- **File created/updated:** Path to documentation file +- **Cross-references added:** List of files with new links +- **Sidebar updates:** Changes to navigation +- **Key decisions:** Any important choices made about structure or content +- **Follow-up recommendations:** Related APIs that might need documentation + +## Important Guidelines + +### Accuracy is Paramount + +- **Never guess** - if you're unsure, trace through the source code again +- **Verify types** - facet vs non-facet values must be correct +- **Test understanding** - explain the behavior in your own words and verify +- **Ask questions** - if something is unclear, ask rather than assume + +### Write for Users, Not Implementers + +- Focus on "how to use" not "how it works internally" +- Include internal details only when they affect behavior +- Explain concepts in terms users understand +- Avoid implementation jargon unless necessary + +### Examples Are Critical + +- Every example must be complete and runnable +- Use realistic variable names and scenarios +- Show common patterns, not artificial demos +- Include TypeScript types in examples +- Follow conventions from custom instructions (e.g., facet naming, when to use `useFacetWrap` vs `useFacetState`) + +### Maintain Consistency + +- Match existing documentation structure exactly +- Use the same terminology as other docs +- Follow the same code style in examples +- Use the same callout patterns (warnings, notes, tips) + +### Performance Documentation + +For React Facet, performance is a key concern. Always document: + +- Re-render behavior (when does the component/hook cause re-renders?) +- Facet reference stability (when are new facet references created?) +- Comparison with alternatives +- Performance gotchas or optimization tips + +## Docusaurus-Specific Guidelines + +This project uses Docusaurus. Be aware of: + +### Frontmatter + +```markdown +--- +title: API Name +sidebar_label: Short Label +description: Brief description for SEO +--- +``` + +### Code Blocks + +`````markdown +````typescript +// Code here +\``` +```` +````` + +```` + +### Callouts + +```markdown +:::note +Standard information +::: + +:::tip +Helpful advice +::: + +:::warning +Important warnings +::: + +:::danger +Critical warnings +::: +``` + +### Internal Links + +```markdown +[Link Text](./other-page.md) +[Link Text](../category/page.md) +``` + +## Iteration Protocol + +This is a collaborative, iterative process: + +1. **Present research findings** β†’ Get confirmation on understanding +2. **Present documentation structure** β†’ Get approval on organization +3. **Present draft documentation** β†’ Get detailed feedback +4. **Refine based on feedback** β†’ Present updated version +5. **Repeat steps 3-4** until approved +6. **Present cross-reference plan** β†’ Get approval +7. **Make all updates** β†’ Get final verification + +**At each step:** + +- Explain your reasoning +- Ask specific questions when uncertain +- Acknowledge feedback explicitly +- Show how you addressed each point +- Request confirmation before proceeding + +## Success Criteria + +Documentation is complete when: + +- βœ… User confirms technical accuracy +- βœ… All common use cases are covered with examples +- βœ… Performance characteristics are clearly documented +- βœ… Edge cases and gotchas are explained +- βœ… Style and structure match existing documentation +- βœ… Cross-references are complete and bidirectional +- βœ… Sidebar navigation is updated +- βœ… Self-review checklist is complete +- βœ… User gives final approval + +## Remember + +- **Quality over speed** - take the time to understand deeply +- **Iterate, don't perfect** - expect multiple rounds of refinement +- **Ask questions** - better to clarify than to guess +- **Verify everything** - trace through code, don't assume +- **Stay aligned** - maintain consistency with existing documentation + +Good documentation is the foundation for good custom instructions. Take the time to get this right. +```` diff --git a/.github/prompts/fix-instructions.prompt.md b/.github/prompts/fix-instructions.prompt.md new file mode 100644 index 00000000..b3b05086 --- /dev/null +++ b/.github/prompts/fix-instructions.prompt.md @@ -0,0 +1,228 @@ +--- +description: Check if the Copilot custom instructions are in sync with the codebase, and fix any desyncs +--- + +You are helping maintain the **Copilot custom instructions** for this repository (NOT the documentation site in `docs/`). + +## Task + +Check if the Copilot custom instructions are in sync with the codebase, and if not, fix any desyncs. + +## Steps + +1. **Run the sync check scripts:** + + - Execute `./scripts/check-copilot-instructions-sync.sh` + - Execute `./scripts/check-public-api-instructions-sync.sh` + +2. **Analyze the output** to identify specific issues: + + - Missing hook/component in custom instructions + - Hooks/components mentioned in instructions but not in code + - Package structure mismatches + - Missing critical concepts (NO_VALUE, dual dependencies, etc.) + - Internal API leakage in public API instructions + +3. **Check for existing public documentation** for missing items: + + For each missing hook/component/API identified in step 2: + + - Search for documentation in `docs/docs/api/` for the missing item + - Check if comprehensive public-facing documentation exists + - Verify the documentation includes: + - Clear API reference with types + - Multiple usage examples + - Performance characteristics + - When to use vs alternatives + + **If documentation is missing or incomplete:** + +This is a strict, _fatal_ stop: do not edit or update `.github/copilot-instructions.md` or `.github/copilot-instructions-public-api.md` when any missing API lacks complete public docs. The agent must refuse to proceed and must produce a precise, actionable report and slash-commands for the maintainer to create the missing docs. + +Required behavior when docs are missing or incomplete: + +1. Immediately STOP any attempt to modify the instruction files. Do not proceed to step 4. + +2. Produce a clear report listing every missing or incomplete API. For each item include **all** of the following: + + + - Implementation source file path (absolute or repo-relative), e.g. `packages/@react-facet/core/src/components/Unwrap.ts` + - Test file path(s) if present, e.g. `packages/@react-facet/core/src/components/Unwrap.spec.tsx` + - Short summary (1-2 sentences) of what the API does, taken from JSDoc or the implementation + - Exact doc-paths that were searched (so maintainers can see what you looked for), e.g. `docs/docs/api/components/Unwrap.md` + +3. For each missing or incomplete API, generate one or more ready-to-run Copilot slash commands that the maintainer can copy & paste to create the missing documentation files. The slash commands must follow this exact format (one command per missing doc): + + + ``` + /create-documentation at <source_path> -> docs/docs/api/<relative_doc_path>.md + ``` + + Examples (exact format): + + ``` + /create-documentation Unwrap component at packages/@react-facet/core/src/components/Unwrap.ts -> docs/docs/api/components/Unwrap.md + ``` + + ``` + /create-documentation Times component at packages/@react-facet/core/src/components/Times.tsx -> docs/docs/api/components/Times.md + ``` + + - The generated doc path must be inside `docs/docs/api/` and mirror the package/component structure when practical. + - If tests exist, include a suggestion to copy the most representative example(s) from the spec file into the new doc. + +4. If docs are present but incomplete, the report must also list the exact missing sections (e.g., "missing function signature", "no usage examples", "no performance notes"), and the slash command should include a short `--template` hint describing required sections (signature, 2 examples, performance notes). + +5. After producing the report and slash-commands, explicitly instruct the user to run the provided slash commands (or otherwise add the required docs) and then re-run this fix process. Do not make any edits to the instruction files until the public docs exist and pass verification. + +6. Verification checks you must perform when re-checking presence of docs (automated rules the agent must follow): + + + - The documentation file exists under `docs/docs/api/` with a markdown header that matches the API name. + - The doc includes at least one fenced code block that shows the component/function signature or usage (a `tsx` or `typescript` code block). + - The doc contains at least one `Example` or `Usage` section with concrete examples. + - The doc includes a short `When to use` or `Notes` section mentioning performance or re-render behavior where applicable. + +If any of the verification checks fail, the agent must repeat the STOP+report cycle (do not proceed). + +**If documentation exists and is comprehensive:** + +- Note the documentation file paths for reference +- Proceed to step 4 + +4. **Research the codebase** for missing items (only if documentation exists): + + For each missing hook/component/API, read: + + - **The public documentation** (`docs/docs/api/**`) as the primary reference: + - This is the authoritative source for how the API should be explained + - Use its examples, explanations, and structure as your guide + - The custom instructions should align with and reference this documentation + - **The source file** to understand implementation in detail: + - Does it use `useFacetUnwrap` internally? (causes re-renders on value changes) + - Does it maintain facet semantics? (only re-renders on mount/unmount) + - What are the actual TypeScript types for parameters and return values? + - Are values kept as facets or unwrapped to regular React state? + - **JSDoc comments** for intended purpose + - **Spec files** (`.spec.ts`/`.spec.tsx`) for real usage examples + - **Related components/hooks** to understand how it fits in the ecosystem + - **Existing custom instructions** to understand: + - Established conventions (e.g., when to use `useFacetWrap` vs `useFacetState`) + - Performance characteristics and re-render behavior + - How similar components/hooks are documented + - Common patterns and anti-patterns + +5. **Fix the issues** by updating the custom instruction files: + + - `.github/copilot-instructions.md` (full internal guide for contributors/Copilot) + - `.github/copilot-instructions-public-api.md` (public API reference for users) + +6. **For each fix:** + + - **Reference the public documentation** (`docs/docs/api/**`) as your primary source: + - Use the same examples and explanations where possible + - Maintain consistency with the public documentation's mental model + - Add references to the documentation files in your custom instructions + - **Read the current codebase thoroughly** to verify documentation accuracy: + - Source file implementation (trace through all function calls) + - Type signatures (what's a facet vs regular value at each step) + - JSDoc comments + - Spec files for real-world usage patterns + - **Review existing custom instructions** before writing: + - Check for established conventions about the API you're documenting + - Review how similar APIs are documented (style, depth, structure) + - Understand the project's mental model (facet semantics, re-render behavior, etc.) + - Note any warnings or gotchas about similar patterns + - **Verify your understanding** by cross-referencing: + - Does the component/hook unwrap facets or maintain facet semantics? + - What causes re-renders? (mount/unmount only, or value changes too?) + - Are there conventions about when to use this vs alternatives? + - Do your code examples follow established patterns from the instructions? + - Does your explanation align with the public documentation? + - Update custom instructions to match code reality and reference documentation + - Preserve existing writing style and formatting + - Update the "Last Updated" date to today's date + +7. **Documentation Quality Standards:** + + When adding or updating API documentation in custom instructions, ensure you match the existing quality level and align with public documentation: + + **For hooks/functions:** + + - Full function signature with TypeScript types + - Clear description of purpose and behavior (reference public docs) + - Multiple code examples showing different use cases (minimum 2-3 examples, ideally from public docs) + - "When to use" / "When NOT to use" guidance + - Comparison with similar APIs when applicable + - Performance implications or warnings (e.g., "causes re-renders") + - Edge cases and gotchas (e.g., NO_VALUE checks required) + - Reference to public documentation file when applicable + + **For components:** + + - Component signature with prop types + - Clear description of purpose and behavior (reference public docs) + - Code examples showing realistic usage (minimum 2 examples, ideally from public docs) + - Performance warnings if applicable (e.g., re-render behavior) + - Comparison with alternative components when applicable + - Integration examples (how it fits with other patterns) + - Best practices for usage + - Reference to public documentation file when applicable + + **Example quality reference:** + Look at existing entries like `useFacetMap`, `useFacetTransition`, or `Mount` component to see the expected depth and style. Your additions should be comparable in: + + - Length (similar sections should have similar detail) + - Number of examples (match the pattern - usually 2-4 examples) + - Level of explanation (match the depth of existing entries) + - Code quality (complete, runnable examples with proper types) + - Alignment with public documentation + +8. **Verify documentation completeness and accuracy:** Before finishing, validate your additions: + + - **Alignment with public documentation:** + + - Does your custom instruction align with the public documentation's explanation? + - Are you using examples from or consistent with the public docs? + - Did you reference the public documentation file path? + - Are there any contradictions between your instructions and the public docs? + + - **Accuracy checks:** + + - Re-read the source code - does your documentation match the actual implementation? + - Check type signatures - are facet vs non-facet values correctly represented? + - Verify re-render behavior - does it unwrap facets or maintain facet semantics? + - Review code examples - do they follow conventions from the custom instructions? + - Test mental model - if you were a user reading this, would you understand when/how to use it? + + - **Completeness checks (compare against similar existing entries):** + - Is your description as clear and detailed? + - Do you have enough code examples? + - Did you explain when to use vs when NOT to use? + - Did you include warnings about performance/behavior? + - Did you maintain consistent formatting with existing docs? + +9. **Verify the fixes** by re-running both check scripts + +10. **Show a summary** of what was changed + +## Important Notes + +- These are **custom instructions for GitHub Copilot**, not user-facing documentation +- The documentation site is in `docs/` - **this is the primary reference source** +- **Documentation-first approach**: If public documentation doesn't exist for an API, STOP and recommend creating documentation first before updating custom instructions +- Code is the source of truth for implementation - use it to verify documentation accuracy +- Both instruction files should document the same APIs, but with different levels of detail: + - **Full instructions** (`.github/copilot-instructions.md`): Includes internal details, testing, repository structure, development workflows, and contributor guidance + - **Public API** (`.github/copilot-instructions-public-api.md`): Only user-facing APIs, usage patterns, and best practices - NO testing utilities or internal APIs + +**Quality over speed:** Take time to understand the code, review public documentation thoroughly, and write comprehensive custom instructions that align with and reference the public docs. Well-aligned documentation is more valuable than quick additions. + +## Workflow Summary + +1. Run sync checks β†’ Identify missing items +2. **Check for public docs** β†’ If missing, STOP and report +3. If docs exist β†’ Research codebase + Read public docs +4. Write custom instructions aligned with public docs +5. Verify alignment, accuracy, and completeness +6. Re-run sync checks to confirm diff --git a/.github/workflows/validate-copilot-instructions.yml b/.github/workflows/validate-copilot-instructions.yml new file mode 100644 index 00000000..f91c06f5 --- /dev/null +++ b/.github/workflows/validate-copilot-instructions.yml @@ -0,0 +1,15 @@ +name: Validate Copilot Instructions + +on: + pull_request: + push: + branches: [main] + +jobs: + check-copilot-instructions-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Check Copilot instructions sync with codebase + run: bash scripts/check-copilot-instructions-sync.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b91e4920..d2e9b066 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,7 @@ +# Contributing to Ore UI + +Thank you for your interest in contributing to Ore UI! This guide will help you get started. + # Creating a new release Currently the process of updating the version of the packages is mostly manual. Before you start, first you need to make sure you have the permissions to publish the packages. @@ -15,7 +19,7 @@ Currently the process of updating the version of the packages is mostly manual. - Choose the tag and click the button "Auto-generate release notes" - Click "publish release" πŸš€ -# Release candidate +## Release candidate - Make sure that you are logged in into your `npm` account. Use the command `yarn npm login` on the project folder to do this. - Create a branch including the changes for the release candidate and name the branch to the same thing as the upcoming release, eg: `v0.4`. @@ -26,3 +30,53 @@ Currently the process of updating the version of the packages is mostly manual. - Push commit and the tag `git push --follow-tags`. - Publish the packages by running `yarn publish --tag rc` (it will also build the packages). - We are currently not doing release notes for release candidates so you are all done! πŸŽ‰ + +# Copilot Custom Instructions Maintenance + +## Using Copilot Slash Command (Recommended) + +The easiest way to check and fix the Copilot custom instructions is using the slash command: + +``` +/fix-instructions +``` + +This will: + +1. Run both sync check scripts +2. Identify desyncs between code and custom instructions +3. Automatically fix issues in both instruction files +4. Verify the fixes +5. Show you a summary of changes + +## Using Scripts Directly + +You can also run the validation scripts directly: + +```bash +# Check if custom instructions are in sync with code +./scripts/check-copilot-instructions-sync.sh +./scripts/check-public-api-instructions-sync.sh + +# Or check both at once +./scripts/check-copilot-instructions-sync.sh && \ +./scripts/check-public-api-instructions-sync.sh +``` + +See [Scripts README](scripts/README.md) for more details. + +## When to Update Custom Instructions + +Update the Copilot custom instructions when you: + +- Add/remove/modify public APIs (hooks, components, utilities) +- Update package structure or import paths +- Add new best practices or patterns +- Change how features work + +Both instruction files should be kept in sync with the codebase: + +- `.github/copilot-instructions.md` - Full internal guide for contributors/Copilot +- `.github/copilot-instructions-public-api.md` - Public API reference for users + +**Note:** These are custom instructions for GitHub Copilot, not the user-facing documentation site (which is in `docs/`). diff --git a/docs/docs/api/hooks/use-facet-unwrap.md b/docs/docs/api/hooks/use-facet-unwrap.md index 3266106d..19524364 100644 --- a/docs/docs/api/hooks/use-facet-unwrap.md +++ b/docs/docs/api/hooks/use-facet-unwrap.md @@ -345,3 +345,4 @@ const Component4 = ({ facet }: { facet: Facet<string[]> }) => { - [Mount](../mount-components#mount) - For conditional mounting without unwrapping - [useFacetCallback](./use-facet-callback) - For accessing facet values in callbacks - [Equality Checks](../equality-checks) - Available equality check functions +- [Unwrap component](../mount-components#unwrap) - Component wrapper for unwrapping facet values in JSX diff --git a/docs/docs/api/mount-components.md b/docs/docs/api/mount-components.md index 91a7803a..0a5bbb98 100644 --- a/docs/docs/api/mount-components.md +++ b/docs/docs/api/mount-components.md @@ -221,3 +221,132 @@ const UserData = ({ name, middlename }: UserDataProps) => { ) } ``` + +## `Unwrap` + +The `Unwrap` component extracts the plain value from a `Facet` and renders children with that unwrapped value. Use it when you need to pass a plain value into JSX (for example to a third-party component), or when you want to limit the scope of a React re-render to a small subtree. + +Important: `Unwrap` uses `useFacetUnwrap` internally, which creates React state for the unwrapped value. However, `Unwrap` typically produces a smaller re-render scope than calling `useFacetUnwrap` at the top level of a component because it confines the stateful re-render to its children. + +Signature: + +```tsx +<Unwrap data={someFacet}>{(value) => <SomeChild value={value} />}</Unwrap> +``` + +Basic example: + +```tsx twoslash +// @esModuleInterop +import { useFacetState, Unwrap } from '@react-facet/core' + +const Example = () => { + const [nameFacet] = useFacetState<string | undefined>('Alex') + + return <Unwrap data={nameFacet}>{(name) => <div>Hello, {name}!</div>}</Unwrap> +} +``` + +When to use: + +- Interfacing with third-party components that accept plain values (not facets). +- Local rendering scenarios where a small, controlled re-render is acceptable. +- Prefer `Unwrap` over calling `useFacetUnwrap` at the top level of your component: `Unwrap` limits the re-render to its children instead of making the entire component re-render on facet updates. + +Prefer `Unwrap` over multiple `Mount` components when you need to choose between mutually-exclusive branches. For example, instead of creating two `Mount`s for opposite conditions: + +```tsx twoslash +// @esModuleInterop +import { useFacetState, useFacetMap, Mount } from '@react-facet/core' +const Foo = () => <div>Foo</div> +const Bar = () => <div>Bar</div> + +const Bad = () => { + const [condFacet] = useFacetState<boolean | undefined>(true) + + return ( + <> + <Mount when={useFacetMap((c) => !!c, [], [condFacet])}> + <Foo /> + </Mount> + <Mount when={useFacetMap((c) => !c, [], [condFacet])}> + <Bar /> + </Mount> + </> + ) +} +``` + +Use a single `Unwrap` that renders one branch or the other: + +```tsx twoslash +// @esModuleInterop +import { useFacetState, Unwrap } from '@react-facet/core' +const Foo = () => <div>Foo</div> +const Bar = () => <div>Bar</div> + +const Good = () => { + const [condFacet] = useFacetState<boolean | undefined>(true) + + return <Unwrap data={condFacet}>{(cond) => (cond ? <Foo /> : <Bar />)}</Unwrap> +} + +// Note: Foo and Bar are declared in the previous example for brevity and re-use in this file. +``` + +Why this matters: + +- Each `Mount` internally manages mounting state; using two `Mount`s for opposite conditions can create more internal React state and may result in multiple re-renders for the same logical switch. A single `Unwrap` keeps the state and re-render scope smaller. +- When opposing conditions change at slightly different times, multiple `Mount`s can briefly both mount or unmount, causing transient visual glitches. A single `Unwrap` evaluates the condition once and consistently renders the chosen branch. + +When NOT to use: + +- For binding dynamic values to the DOM β€” prefer `fast-*` components or facet-aware patterns instead. +- For simple single-condition mounts where `Mount` or `With` are the clearer, more efficient choice. Use `Unwrap` when you need a concise multi-branch conditional rendered from a single facet. + +See also: + +- [useFacetUnwrap](./hooks/use-facet-unwrap) - The hook used internally (performance warning applies) + +## `Times` + +Renders a child-producing function a fixed number of times based on a `Facet<number>`. + +The `Times` component accepts a single required prop, `count`, which must be a `Facet<number>`. Its `children` prop is a function that will be called for each index from `0` to `count - 1` and should return a `ReactElement` or `null`. + +This component is useful when you want to repeat a piece of UI a dynamic number of times while keeping updates scoped to the repeated subtree. + +Signature: + +```tsx +<Times count={countFacet}>{(index, count) => <Item key={index} index={index} total={count} />}</Times> +``` + +Basic example: + +```tsx twoslash +// @esModuleInterop +import { useFacetState, Times, NO_VALUE } from '@react-facet/core' + +const Example = () => { + const [countFacet, setCount] = useFacetState(3) + + return ( + <div> + <Times count={countFacet}>{(index) => <div key={index}>Row {index}</div>}</Times> + <button onClick={() => setCount((c) => (c !== NO_VALUE ? c + 1 : 1))}>Add</button> + </div> + ) +} +``` + +Performance considerations + +- `Times` subscribes to the provided `count` facet. When the numeric value changes, `Times` will re-evaluate how many children to render. +- If the `count` increases or decreases, the component mounts or unmounts the corresponding child subtrees, which can be more expensive than updating existing children. Prefer keeping the array size stable when possible (e.g., reuse items) to avoid mounting churn. +- The child function receives the `index` and the current `count` as plain values. If you need facet-aware children, use `Map` with an array facet instead. + +When to use + +- Use `Times` when you need to repeat a UI fragment a variable number of times determined by a numeric facet. +- Use `Map` instead when you have an array of data and want each item wrapped in a `Facet` (for per-item updates without remounting the whole list when data changes but length stays the same). diff --git a/docs/yarn.lock b/docs/yarn.lock index b4f461f9..1c8886d1 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2685,7 +2685,7 @@ __metadata: dependencies: react-reconciler: ^0.28.0 peerDependencies: - "@react-facet/core": 18.2.0 + "@react-facet/core": 18.4.0 react: ^18.2.0 languageName: node linkType: soft @@ -2694,7 +2694,7 @@ __metadata: version: 0.0.0-use.local resolution: "@react-facet/shared-facet@portal:../packages/@react-facet/shared-facet::locator=ore-ui-docs%40workspace%3A." peerDependencies: - "@react-facet/core": 18.2.0 + "@react-facet/core": 18.4.0 react: ^18.2.0 languageName: node linkType: soft diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..9f42719e --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,250 @@ +# Copilot Custom Instructions Sync Check Scripts + +This directory contains automated validation scripts to ensure the Copilot custom instruction files stay synchronized with the codebase. + +## Quick Start: VS Code Copilot Slash Command + +The easiest way to check and fix the custom instructions is using the Copilot slash command: + +``` +/fix-instructions +``` + +This will: + +1. Run both sync check scripts +2. Identify all desyncs between code and custom instructions +3. Automatically fix the issues in both instruction files +4. Verify the fixes by re-running the checks +5. Show you a summary of changes + +The command is configured in `.github/prompts/fix-instructions.prompt.md`. + +## Available Scripts + +### `check-copilot-instructions-sync.sh` + +Validates `.github/copilot-instructions.md` (full internal guide) against the codebase. + +**Usage:** + +```bash +./scripts/check-copilot-instructions-sync.sh +``` + +**What it checks:** + +- βœ… All hooks mentioned in custom instructions exist in code +- βœ… All hooks in code are documented in custom instructions +- βœ… Core components (`Map`, `Mount`, `With`) are documented +- βœ… All packages exist +- βœ… Import paths are correct + +**Exit codes:** + +- `0` - All checks passed +- `1` - Custom instructions drift detected + +--- + +### `check-public-api-instructions-sync.sh` + +Validates `.github/copilot-instructions-public-api.md` (public API reference) against the codebase. + +**Usage:** + +```bash +./scripts/check-public-api-instructions-sync.sh +``` + +**What it checks:** + +- βœ… All documented hooks exist in the codebase +- βœ… All public hooks from `core/src/hooks/index.ts` are documented +- βœ… Core components are properly documented +- βœ… Package imports are correct +- βœ… Critical concepts explained (NO_VALUE, dual dependencies) +- βœ… Equality checks are documented +- βœ… No internal/private APIs leaked to public instructions +- βœ… Testing utilities not featured (they're internal) +- βœ… Examples use correct imports + +**Exit codes:** + +- `0` - All checks passed +- `1` - Custom instructions drift detected + +## Note about code fence checks + +When these scripts search for Markdown code fences like ` typescript the shell can accidentally interpret backticks (``) as command substitution if the pattern is double-quoted. The scripts therefore use single-quoted grep patterns (for example: grep -q ' `typescript') to avoid this class of errors. If you modify the scripts, keep that in mind to prevent runtime failures. + +--- + +## CI/CD Integration + +Add both scripts to your continuous integration workflow to catch custom instructions drift automatically: + +```yaml +# .github/workflows/ci.yml +name: CI + +on: [push, pull_request] + +jobs: + instructions-validation: + name: Validate Copilot Custom Instructions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Check Full Instructions Sync + run: ./scripts/check-copilot-instructions-sync.sh + + - name: Check Public API Instructions Sync + run: ./scripts/check-public-api-instructions-sync.sh +``` + +--- + +## When to Run + +### During Development + +Run these scripts whenever you: + +- Add/remove/modify public APIs (hooks, components, utilities) +- Update package structure +- Change import paths +- Add new best practices or patterns + +### Before Release + +Always run both scripts as part of your release checklist: + +```bash +# Quick check both instruction files +./scripts/check-copilot-instructions-sync.sh && \ +./scripts/check-public-api-instructions-sync.sh && \ +echo "βœ… All custom instructions are in sync!" +``` + +### In Pull Requests + +Configure GitHub Actions to run these checks on every PR to prevent custom instructions drift from being merged. + +--- + +## Fixing Drift + +When a script reports errors, you can either use the `/fix-instructions` slash command or follow these manual steps: + +### Using Slash Command (Recommended) + +``` +/fix-instructions +``` + +Copilot will automatically fix all desyncs and update both instruction files. + +### Manual Fix Steps + +#### 1. Identify the Issue + +The script output shows exactly what's missing or incorrect: + +``` +⚠️ Hook 'useFacetNewFeature' exists in code but not documented in public API docs +``` + +#### 2. Update Custom Instructions + +- For **new public APIs**: Add full documentation with examples +- For **removed APIs**: Remove all references +- For **changed APIs**: Update signatures and examples + +#### 3. Update "Last Updated" Date + +In both instruction files, update the maintenance section: + +```markdown +> **Last Updated**: DD Month YYYY +``` + +#### 4. Re-run Validation + +```bash +./scripts/check-public-api-instructions-sync.sh +``` + +#### 5. Commit Changes + +Include custom instructions updates in the same commit/PR as code changes when possible. + +--- + +## Maintenance + +### Script Maintenance + +When the project structure changes: + +1. **New package added**: Update package check lists in both scripts +2. **New export added**: Ensure it's checked against documentation +3. **Critical concept added**: Add to the "critical concepts" check + +### Keeping Scripts in Sync + +The public API script is intentionally stricter than the full instructions script: + +- **Full script**: Checks everything (including internal details) +- **Public API script**: Only checks user-facing APIs + enforces no private API leakage + +When updating one script, consider if the other needs similar changes. + +--- + +## Troubleshooting + +### Script Won't Execute + +```bash +chmod +x ./scripts/check-copilot-instructions-sync.sh +chmod +x ./scripts/check-public-api-instructions-sync.sh +``` + +### False Positives + +If a hook legitimately shouldn't be in public instructions (e.g., it's internal), ensure: + +1. It's not exported from `packages/@react-facet/core/src/hooks/index.ts` +2. Or, update the script's exclusion logic + +### Script Errors + +Both scripts use `set -e` to fail fast. If a command fails unexpectedly, check: + +- File paths are correct +- grep patterns are accurate +- The repository structure hasn't changed significantly + +--- + +## Related Files + +- `.github/copilot-instructions.md` - Full internal guide for contributors/Copilot +- `.github/copilot-instructions-public-api.md` - Public API reference for users +- `.github/prompts/fix-instructions.prompt.md` - Slash command configuration +- `docs/` - User-facing documentation site (separate from custom instructions) + +--- + +## Philosophy + +These scripts embody the principle: **Custom instructions should be treated as code**. Just like tests verify code correctness, these scripts verify custom instructions accuracy. + +By automating custom instructions validation, we: + +- βœ… Prevent stale custom instructions +- βœ… Catch missing documentation early +- βœ… Ensure consistency between code and custom instructions +- βœ… Make custom instructions a first-class citizen in the development process diff --git a/scripts/check-copilot-instructions-sync.sh b/scripts/check-copilot-instructions-sync.sh new file mode 100755 index 00000000..d71cdb4f --- /dev/null +++ b/scripts/check-copilot-instructions-sync.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Check for documentation drift between code and Copilot instructions +# +# This script validates that .github/copilot-instructions.md stays in sync with the codebase. +# It is a thin wrapper around check-instructions-sync-core.sh. +# +# Exit codes: +# 0 - All checks passed +# 1 - Documentation drift detected + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Call the core script with full mode +exec "$SCRIPT_DIR/check-instructions-sync-core.sh" ".github/copilot-instructions.md" "full" diff --git a/scripts/check-instructions-sync-core.sh b/scripts/check-instructions-sync-core.sh new file mode 100755 index 00000000..8549f8ae --- /dev/null +++ b/scripts/check-instructions-sync-core.sh @@ -0,0 +1,330 @@ +#!/bin/bash +# Core documentation drift checker +# +# This script validates that instruction files stay in sync with the codebase. +# It is designed to be called by wrapper scripts that specify which file to check +# and what validation mode to use. +# +# Usage: +# ./check-instructions-sync-core.sh <instructions_file> <mode> +# +# Arguments: +# instructions_file - Path to the instructions markdown file +# mode - Validation mode: "full" or "public-api" +# +# Exit codes: +# 0 - All checks passed +# 1 - Documentation drift detected +# 2 - Invalid arguments + +set -e + +# ============================================================================ +# ARGUMENT PARSING +# ============================================================================ +if [ $# -ne 2 ]; then + echo "Usage: $0 <instructions_file> <mode>" + echo " instructions_file: Path to instructions markdown file" + echo " mode: 'full' or 'public-api'" + exit 2 +fi + +INSTRUCTIONS_FILE="$1" +MODE="$2" + +if [ ! -f "$INSTRUCTIONS_FILE" ]; then + echo "Error: Instructions file '$INSTRUCTIONS_FILE' not found" + exit 2 +fi + +if [ "$MODE" != "full" ] && [ "$MODE" != "public-api" ]; then + echo "Error: Mode must be 'full' or 'public-api'" + exit 2 +fi + +ERRORS=0 + +# Set display messages based on mode +if [ "$MODE" = "public-api" ]; then + DOC_TYPE="public API documentation" + DOC_NAME="public API docs" +else + DOC_TYPE="documentation" + DOC_NAME="instructions" +fi + +echo "πŸ” Checking for $DOC_TYPE drift..." +echo "" + +# ============================================================================ +# CHECK 1: Verify all hooks mentioned in instructions exist in the codebase +# ============================================================================ +echo "πŸ“‹ Checking hooks..." +HOOKS_IN_DOCS=$(grep -o "useFacet[A-Za-z]*" "$INSTRUCTIONS_FILE" | sort -u) +for hook in $HOOKS_IN_DOCS; do + # Search for the hook as either a function export or const export + if ! grep -r "export.*function $hook" packages/@react-facet/core/src/hooks/ > /dev/null 2>&1 && \ + ! grep -r "export.*$hook.*=" packages/@react-facet/core/src/hooks/ > /dev/null 2>&1; then + echo "⚠️ Hook '$hook' mentioned in $DOC_NAME but not found in code" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ $hook" + fi +done + +# ============================================================================ +# CHECK 2: Verify all hooks in code are documented in instructions +# ============================================================================ +echo "" +echo "πŸ“‹ Checking if code hooks are documented..." + +# Get all hooks from the core package's hooks index.ts (this lists all public hooks) +if [ -f "packages/@react-facet/core/src/hooks/index.ts" ]; then + HOOKS_IN_CODE=$(grep "export.*from" packages/@react-facet/core/src/hooks/index.ts | \ + grep -o "useFacet[A-Za-z]*" | sort -u) + + for hook in $HOOKS_IN_CODE; do + if ! grep -q "$hook" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Hook '$hook' exists in code but not documented in $DOC_NAME" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ $hook" + fi + done +else + echo "⚠️ Cannot find hooks/index.ts to verify hooks" + ERRORS=$((ERRORS + 1)) +fi + +# ============================================================================ +# CHECK 3: Verify core components are documented +# ============================================================================ +echo "" +echo "πŸ“¦ Checking components..." + +# Dynamically get all exported components from the components index.ts +if [ -f "packages/@react-facet/core/src/components/index.ts" ]; then + COMPONENTS=$(grep "export.*from" packages/@react-facet/core/src/components/index.ts | \ + grep -o "'\.\/[A-Za-z]*'" | \ + sed "s/'\.\/\(.*\)'/\1/" | \ + sort -u) + + for component in $COMPONENTS; do + # Check for component as JSX tag (<Component) or in backticks (`Component`) + # This prevents false positives like "Unwrap" matching "useFacetUnwrap" + if ! grep -q "<$component" "$INSTRUCTIONS_FILE" && ! grep -q "\`$component\`" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Component '$component' exists in code but not documented in $DOC_NAME" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ $component" + fi + done +else + echo "⚠️ Cannot find components/index.ts to verify components" + ERRORS=$((ERRORS + 1)) +fi + +# ============================================================================ +# CHECK 4: Verify packages exist and are documented +# ============================================================================ +echo "" +echo "πŸ“¦ Checking packages..." + +if [ "$MODE" = "public-api" ]; then + # Public API mode: only check public packages + PACKAGES="core dom-fiber shared-facet" + + for package in $PACKAGES; do + if [ ! -d "packages/@react-facet/$package" ]; then + echo "⚠️ Public package '$package' mentioned in docs but doesn't exist" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ @react-facet/$package" + fi + + # Verify package is documented + if ! grep -q "@react-facet/$package" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Package '@react-facet/$package' exists but not documented in $DOC_NAME" + ERRORS=$((ERRORS + 1)) + fi + done + + # Verify testing library is NOT featured in public docs + if grep -q "@react-facet/dom-fiber-testing-library" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Testing library should not be in public API docs (it's for internal use)" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ Testing library correctly omitted from public API docs" + fi +else + # Full mode: check all packages including testing library + PACKAGES="core dom-fiber dom-fiber-testing-library shared-facet" + + for package in $PACKAGES; do + if [ ! -d "packages/@react-facet/$package" ]; then + echo "⚠️ Package '$package' mentioned in instructions but doesn't exist" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ @react-facet/$package" + fi + done +fi + +# ============================================================================ +# CHECK 5: Verify import examples reference real packages +# ============================================================================ +echo "" +echo "πŸ“₯ Checking import paths..." + +# Check @react-facet/core imports +if grep -q "from '@react-facet/core'" "$INSTRUCTIONS_FILE"; then + if [ -d "packages/@react-facet/core" ]; then + echo "βœ“ @react-facet/core imports" + fi +fi + +# Check @react-facet/dom-fiber imports +if grep -q "from '@react-facet/dom-fiber'" "$INSTRUCTIONS_FILE"; then + if [ -d "packages/@react-facet/dom-fiber" ]; then + echo "βœ“ @react-facet/dom-fiber imports" + fi +fi + +# Check @react-facet/shared-facet imports +if grep -q "from '@react-facet/shared-facet'" "$INSTRUCTIONS_FILE"; then + if [ -d "packages/@react-facet/shared-facet" ]; then + echo "βœ“ @react-facet/shared-facet imports" + fi +fi + +# Only check testing library imports in full mode +if [ "$MODE" = "full" ]; then + if grep -q "from '@react-facet/dom-fiber-testing-library'" "$INSTRUCTIONS_FILE"; then + if [ -d "packages/@react-facet/dom-fiber-testing-library" ]; then + echo "βœ“ @react-facet/dom-fiber-testing-library imports" + fi + fi +fi + +# ============================================================================ +# CHECK 6: Public API specific checks +# ============================================================================ +if [ "$MODE" = "public-api" ]; then + echo "" + echo "πŸ”‘ Checking critical concepts..." + + # Check that NO_VALUE is explained + if ! grep -q "NO_VALUE" "$INSTRUCTIONS_FILE"; then + echo "⚠️ NO_VALUE concept not documented (critical for public API)" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ NO_VALUE" + fi + + # Check that dual dependency arrays are explained + if ! grep -q "Two Dependency Arrays" "$INSTRUCTIONS_FILE" && \ + ! grep -q "two dependency arrays" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Dual dependency arrays not explained (critical pattern)" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ Dual dependency arrays" + fi + + # Check that fast-* components are documented + if ! grep -q "fast-" "$INSTRUCTIONS_FILE"; then + echo "⚠️ fast-* components not documented (core feature)" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ fast-* components" + fi + + # Check that createRoot is documented (not deprecated render) + if grep -q "## Mounting" "$INSTRUCTIONS_FILE" || grep -q "### Mounting" "$INSTRUCTIONS_FILE"; then + if ! grep -q "createRoot" "$INSTRUCTIONS_FILE"; then + echo "⚠️ createRoot not documented (primary mounting API)" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ createRoot" + fi + fi + + echo "" + echo "βš–οΈ Checking equality checks..." + for check in "strictEqualityCheck" "shallowObjectEqualityCheck" "shallowArrayEqualityCheck" "defaultEqualityCheck"; do + if ! grep -q "$check" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Equality check '$check' not documented" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ $check" + fi + done + + echo "" + echo "πŸ”’ Checking for internal/private API leakage..." + + # batch function is marked @private and should not be in public docs + if grep -q "batch(" "$INSTRUCTIONS_FILE" && ! grep -q "batchTransition" "$INSTRUCTIONS_FILE"; then + # Allow mentions of batch in context, but not as a recommended API + if grep -q "use.*batch" "$INSTRUCTIONS_FILE" || grep -q "call.*batch" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Private 'batch' function appears to be recommended in public API docs" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ batch function not recommended" + fi + else + echo "βœ“ batch function not recommended" + fi + + # Testing utilities should not be featured + if grep -q "render.*from.*testing-library" "$INSTRUCTIONS_FILE"; then + echo "⚠️ Testing library utilities should not be in public API docs" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ Testing utilities not featured" + fi + + echo "" + echo "πŸ“ Checking code examples..." + + # Check that examples import from correct packages + # Use single quotes to avoid shell command substitution from backticks + if grep -q '```typescript' "$INSTRUCTIONS_FILE" || grep -q '```tsx' "$INSTRUCTIONS_FILE"; then + # Check for common mistakes in examples + if grep -A 5 "fast-text" "$INSTRUCTIONS_FILE" | grep -q "import.*from '@react-facet/core'" | head -1; then + # Verify dom-fiber is also imported when fast-* components are used + if ! grep -B 5 "fast-text" "$INSTRUCTIONS_FILE" | grep -q "from '@react-facet/dom-fiber'" | head -1; then + echo "⚠️ Examples use fast-* components without importing dom-fiber" + ERRORS=$((ERRORS + 1)) + else + echo "βœ“ Examples properly import dom-fiber for fast-* components" + fi + else + echo "βœ“ Examples structure looks correct" + fi + fi +fi + +# ============================================================================ +# FINAL RESULT +# ============================================================================ +echo "" +if [ $ERRORS -eq 0 ]; then + echo "βœ… Documentation sync check passed!" + exit 0 +else + echo "❌ Found $ERRORS potential documentation drift issue(s)" + echo "" + echo "Please update $INSTRUCTIONS_FILE to match the current codebase." + + if [ "$MODE" = "public-api" ]; then + echo "" + echo "When updating:" + echo " 1. Verify all public hooks are documented" + echo " 2. Ensure critical concepts (NO_VALUE, dual deps) are clear" + echo " 3. Update examples to match current API" + echo " 4. Remove any internal/private API references" + echo " 5. Update the 'Last Updated' date" + fi + + exit 1 +fi diff --git a/scripts/check-public-api-instructions-sync.sh b/scripts/check-public-api-instructions-sync.sh new file mode 100755 index 00000000..41b5096f --- /dev/null +++ b/scripts/check-public-api-instructions-sync.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Check for documentation drift between code and public API instructions +# +# This script validates that .github/copilot-instructions-public-api.md stays in sync with the codebase. +# It is a thin wrapper around check-instructions-sync-core.sh. +# +# Exit codes: +# 0 - All checks passed +# 1 - Documentation drift detected + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Call the core script with public-api mode +exec "$SCRIPT_DIR/check-instructions-sync-core.sh" ".github/copilot-instructions-public-api.md" "public-api"