@mojang/ore-ui
GitHub Copilot Instructions for Ore UI / React Facet
Install
agr install @mojang/ore-ui --target copilotWrites 1 file into .github/copilot-instructions.md, pinned to git-9cd7b496.
- .github/copilot-instructions.md
Document
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.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.shand./scripts/check-public-api-instructions-sync.shto 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.
// β WRONG - TypeScript ERROR!
const value = useFacetUnwrap(numberFacet)
const doubled = value * 2 // Error: NO_VALUE is not a number
const [items, setItems] = useFacetState<string[]>([])
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 returnsT | NO_VALUE- Setter callbacks β always receive
T | NO_VALUE - Check
!== NO_VALUEbefore using the value
2. π¨ CRITICAL: Overusing useFacetUnwrap
Problem: useFacetUnwrap causes React re-renders, defeating the entire performance benefit of facets.
// β WRONG - Causes re-renders, defeats facet purpose!
const value = useFacetUnwrap(facet)
return <div>{value}</div>
// β
CORRECT - Use fast-text, no re-renders
return <fast-text text={facet} />
// β WRONG - Unwrapping for conditional rendering
const isVisible = useFacetUnwrap(isVisibleFacet)
if (isVisible !== NO_VALUE && !isVisible) return null
// β
CORRECT - Use Mount component
<Mount when={isVisibleFacet}>
<ExpensiveComponent />
</Mount>
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.
// β 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):
import { useFacetState, useFacetMap, useFacetEffect, NO_VALUE, shallowObjectEqualityCheck } from '@react-facet/core'
Renderer imports (for fast-* components):
import { createRoot } from '@react-facet/dom-fiber'
// fast-div, fast-text, etc. are available globally when using dom-fiber
β οΈ CRITICAL: Use
createRoot(notrender) for all new code. Therendermethod is deprecated.
Testing imports:
import { render, act } from '@react-facet/dom-fiber-testing-library'
Note: In testing,
renderfrom the testing library is still used. The deprecatedrenderis only from@react-facet/dom-fiberitself.
Gameface integration:
import { useSharedFacet, SharedFacetContext } from '@react-facet/shared-facet'
File Conventions
- Test files: Co-located with source as
*.spec.tsor*.spec.tsx - Type definitions: Primarily in
types.tsfiles 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
interface Facet<T> {
get: () => T | NoValue
observe: (listener: (value: T) => void) => Unsubscribe
}
interface WritableFacet<T> extends Facet<T> {
set: (value: T) => void
setWithCallback: (callback: (previousValue: T | NoValue) => T | NoValue) => void
}
// useFacetState returns this setter type
type Setter<V> = (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:
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> (T | NO_VALUE), not just T. You must check for NO_VALUE before using the value:
const [itemsFacet, setItems] = useFacetState<string[]>([])
// β 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.
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:
// Accepts Facet<T> or T, always returns Facet<T>
const facet = useFacetWrap(maybeFacetProp)
Use useFacetWrapMemo when you need a stable facet reference:
// 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:
// 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:
// Primarily for testing - creating mock data
const mockFacet = createFacet({
initialValue: 'test-value',
startSubscription: (update) => {
// Custom subscription logic
return () => {} // cleanup
},
equalityCheck: defaultEqualityCheck,
})
Note:
createFacetis a low-level API not intended for typical application code. PreferuseFacetStateoruseFacetWrapin components, or use shared facet sources.
2. Deriving Facets (Composition)
Use useFacetMap for lightweight derived facets:
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.
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:
const fullName = useFacetMap(
(firstName, lastName) => `${firstName} ${lastName}`,
[], // non-facet dependencies
[firstNameFacet, lastNameFacet], // facet dependencies
)
Use useFacetMemo for cached/expensive derived facets:
// 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:
- First array (
deps): Non-facet dependencies (props, local variables, functions) - works like standard React hooks - 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
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
dependenciesarray (non-facet dependencies) β new facet reference - Changes to the
facetsarray (different facet instances) β new facet reference - Changes to the
equalityCheckfunction β 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
createFacetinternally) - β 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:
// 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:
- You identify a performance issue (profiling shows repeated expensive computations)
- You know the facet will have many subscribers (e.g., passed to a list of components)
- The mapping function is clearly expensive (complex calculations, large data processing)
3. Binding Facets to UI
Use fast-* components (requires @react-facet/dom-fiber):
// Direct facet binding - NO reconciliation
<fast-text text={counterFacet} />
<fast-div className={healthClassFacet}>
<fast-input value={usernameFacet} />
</fast-div>
Mix facets and static values:
<fast-div id="static-id" className={dynamicClassFacet} />
4. Side Effects
Use useFacetEffect for facet-based effects:
useFacetEffect(
(playerHealth) => {
if (playerHealth < 20) {
playWarningSound()
}
},
[], // dependencies
[playerHealthFacet],
)
Use useFacetLayoutEffect for synchronous effects (like useLayoutEffect):
useFacetLayoutEffect(
(dimensions) => {
measureAndUpdateLayout(dimensions)
},
[],
[dimensionsFacet],
)
5. Callbacks with Facets
Use useFacetCallback to create callbacks that depend on facet values:
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:
const [itemsFacet, setItems] = useFacetState<string[]>([])
// β 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.
// β οΈ NOT RECOMMENDED - Use useFacetState instead
type State = { count: number }
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }
const reducer = (state: Option<State>, action: Action): Option<State> => {
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.
// β οΈ NOT RECOMMENDED - Use setter callback form instead
type FormData = {
username: string
email: string
}
const [formFacet, setForm] = useFacetState<FormData>({
username: '',
email: '',
})
// Create setters for individual properties
const setUsername = useFacetPropSetter(formFacet, 'username')
const setEmail = useFacetPropSetter(formFacet, 'email')
// Use in components
<input onChange={(e) => setUsername(e.target.value)} />
<input onChange={(e) => setEmail(e.target.value)} />
// β
BETTER - Use setter callback form directly
<input onChange={(e) => 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:
// β WRONG - Causes re-renders, defeats facet purpose!
const isVisible = useFacetUnwrap(isVisibleFacet)
if (isVisible !== NO_VALUE && !isVisible) return null
return <ExpensiveComponent />
// β
CORRECT - Use Mount component
<Mount when={isVisibleFacet}>
<ExpensiveComponent />
</Mount>
Use Mount component for conditional mounting:
<Mount when={isVisibleFacet}>
<ExpensiveComponent />
</Mount>
Use Map component for lists:
;<Map array={itemsFacet}>{(itemFacet, index) => <ItemRow key={index} itemFacet={itemFacet} />}</Map>
// In a separate component to follow Rules of Hooks
const ItemRow = ({ itemFacet }: { itemFacet: Facet<Item> }) => {
const nameFacet = useFacetMap((item) => item.name, [], [itemFacet])
return (
<div>
<fast-text text={nameFacet} />
</div>
)
}
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
Mountcomponents) - Limiting re-render scope when unwrapping is necessary
// Basic usage - passes plain value to children
<Unwrap data={nameFacet}>{(name) => <div>Hello, {name}!</div>}</Unwrap>
// Multi-branch conditional - better than two opposing Mount components
<Unwrap data={conditionFacet}>{(cond) => (cond ? <ComponentA /> : <ComponentB />)}</Unwrap>
// Third-party component integration
<Unwrap data={valueFacet}>{(value) => <ThirdPartyComponent value={value} />}</Unwrap>
Key characteristics:
- Uses
useFacetUnwrapinternally (causes re-renders on value changes) - Re-render scope is limited to children, not entire parent component
- Handles
NO_VALUEautomatically (returnsnullif facet has no value) - Better than multiple
Mountcomponents for mutually-exclusive branches
When to use:
- β Interfacing with third-party components
- β
Multi-branch conditionals (prefer over multiple
Mounts) - β 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
Mountinstead) - β 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.
// Basic usage
;<Times count={countFacet}>
{(index, total) => (
<div key={index}>
Row {index} of {total}
</div>
)}
</Times>
// Dynamic count
const [countFacet, setCount] = useFacetState(3)
return (
<div>
<Times count={countFacet}>{(index) => <div key={index}>Item {index}</div>}</Times>
<button onClick={() => setCount((c) => (c !== NO_VALUE ? c + 1 : 1))}>Add</button>
</div>
)
Key characteristics:
- Uses
Unwrapinternally (re-renders when count changes) - Children function receives
index(0-based) andcountas 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
Mapinstead) - β When you need per-item facets (use
Mapwith 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
startTransitionfunction fromuseFacetTransitionis stable and doesn't need to be in dependency arrays - Batching - Uses
batchTransitioninternally 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
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 (
<div>
{isPending && <div>Processing...</div>}
<fast-input value={inputFacet} />
</div>
)
Use startFacetTransition outside components:
// 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<string[]>([])
const handleLoad = () => {
const newData = generateData()
loadDataAsTransition(setData, newData)
}
return <button onClick={handleLoad}>Load</button>
}
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
isPendingisn't needed (usestartFacetTransition) - β 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
startFacetTransitionfor shared state to avoid provider re-renders - The
startTransitioncallback 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:
// β οΈ 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.
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:
-
Passing to non-facet-aware components (though refactoring the component to accept facets is preferred):
const value = useFacetUnwrap(facet) // Always check for NO_VALUE before using if (value === NO_VALUE) return null return <ThirdPartyComponent value={value} /> -
Conditional mounting - DON'T DO THIS! Use
MountorWithcomponents instead:// β 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 <Mount when={isVisibleFacet}> <ExpensiveComponent /> </Mount>
Common NO_VALUE handling patterns:
// 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:
const PlayerContext = createFacetContext<PlayerData>()
// Provider
<PlayerContext.Provider value={playerFacet}>
<GameUI />
</PlayerContext.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):
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:
// 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:
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:
<T extends Immutable | Function> - 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)
- Type-safe - TypeScript constraint:
-
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
shallowObjectEqualityCheckor similar - For arrays: Always use
shallowArrayEqualityCheckor 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:
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:
// β οΈ 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 receivesTuseFacetUnwrap: ReturnsT | NO_VALUEimmediately, YOU must check forNO_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
batchdirectly - It's marked as@privatein the source code - For transitions, use the public APIs:
useFacetTransitionorstartFacetTransition - The library handles batching automatically - Facet updates are batched internally
Internal implementation detail:
batch- General-purpose batching for facet updatesbatchTransition- Special batching for transitions (used byuseFacetTransition/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:
// β
Use fast-div when binding facet values
<fast-div className={classNameFacet}>
<fast-text text={messageFacet} />
</fast-div>
// β
Use fast-input when value is a facet
<fast-input value={usernameFacet} />
Use regular HTML elements when working with static values or non-facet props:
// β
Regular HTML is fine for static content
<div className="container">
<p>Static text content</p>
<button onClick={handleClick}>Click me</button>
</div>
// β
Mix regular HTML with fast-* when needed
<div className="form">
<label>Username:</label>
<fast-input value={usernameFacet} />
</div>
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 propsfast-text- Text content from a facet (renders as text node, no wrapper element)fast-input- Text input that can bind to facet valuesfast-textarea- Multi-line text input with facet supportfast-img- Images with facet-bindable src and other attributesfast-span- Inline element with facet supportfast-p- Paragraph element with facet supportfast-a- Anchor/link element with facet support
Usage Examples
// fast-* components accept Facet<T> or T for all props
<fast-div className={classFacet} style={{ color: colorFacet, fontSize: '16px' }} onClick={handleClick}>
<fast-text text={messageFacet} />
</fast-div>
// Regular HTML for static structure
<div className="game-ui">
<div className="header">
<h1>Player Stats</h1>
</div>
<div className="stats">
{/* Use fast-text only where facet binding is needed */}
Health: <fast-text text={healthFacet} />
</div>
</div>
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-fiberrenderer
β 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
// β
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
// β
Good - facet at component level
function PlayerHealth() {
const [healthFacet, setHealth] = useFacetState(100)
return <fast-text text={healthFacet} />
}
// β
Good - shared facet via context
const PlayerContext = createFacetContext<Player>()
// β 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:
// β
Good - use type for facet data structures
type PlayerData = {
health: number
mana: number
name: string
}
const [playerFacet, setPlayer] = useFacetState<PlayerData>({
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:
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)
// β 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!)
// β Causes re-renders - defeats the entire purpose of facets!
const value = useFacetUnwrap(facet)
return <div>{value}</div>
// β
Use fast-text with facet - no re-renders
return <fast-text text={facet} />
// β Unwrapping for conditional rendering
const isVisible = useFacetUnwrap(isVisibleFacet)
if (!isVisible) return null
return <ExpensiveComponent />
// β
Use Mount component - scopes re-render
return (
<Mount when={isVisibleFacet}>
<ExpensiveComponent />
</Mount>
)
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 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
const [itemsFacet, setItems] = useFacetState<string[]>([])
// β 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<T> (i.e., T | NO_VALUE), just like useFacetUnwrap. Always check before using the value!
6. Not Using Equality Checks for Objects/Arrays
// β 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
// β WRONG - Hook inside Map callback (nested function)
<Map array={itemsFacet}>
{(itemFacet, index) => (
<div key={index}>
<fast-text text={useFacetMap((item) => item.name, [], [itemFacet])} />
</div>
)}
</Map>
// β 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 <fast-text text={nameFacet} />
// β
ALSO CORRECT - Hook in JSX during render (not in conditional/loop/function)
return <fast-text text={useFacetMap((item) => item.name, [], [itemFacet])} />
// β
BEST PRACTICE - Separate component with hooks at top level
const ItemRow = ({ itemFacet }: { itemFacet: Facet<Item> }) => {
const nameFacet = useFacetMap((item) => item.name, [], [itemFacet])
return <fast-text text={nameFacet} />
}
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
// β 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
useFacetCallbackto access facet values in callbacks - β
Do use
useFacetMapto 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
// β WRONG - fast-div used when className is a static string
<fast-div className="item-header">
<fast-div className="item-name">
<fast-text text={nameFacet} />
</fast-div>
</fast-div>
// β WRONG - fast-span used with no facet bindings
<fast-span>Static label text</fast-span>
// β
CORRECT - Regular HTML for static attributes, fast-text only for facet
<div className="item-header">
<div className="item-name">
<fast-text text={nameFacet} />
</div>
</div>
// β
CORRECT - Regular span for static content
<span>Static label text</span>
// β
CORRECT - Use fast-div ONLY when binding a facet to an attribute
<fast-div className={dynamicClassFacet}>
<fast-text text={contentFacet} />
</fast-div>
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β whenclassName,style, or other attributes are facets<div>β when all attributes are static stringsfast-textβ when text content is a facet- Text nodes β when text content is static
fast-inputβ whenvalueor other attributes are facets<input>β NEVER (usefast-inputinstead, see next pitfall)
10. Unwrapping Facets for Form Inputs When fast-* Alternatives Exist
// β WRONG - Unwrapping causes component re-renders!
const username = useFacetUnwrap(usernameFacet)
const sortBy = useFacetUnwrap(sortByFacet)
return (
<>
<input value={username !== NO_VALUE ? username : ''} onChange={(e) => setUsername(e.target.value)} />
<select value={sortBy !== NO_VALUE ? sortBy : 'name'} onChange={(e) => setSortBy(e.target.value)} />
</>
)
// β
CORRECT - Use fast-input and fast-select (if available), no re-renders
return (
<>
<fast-input value={usernameFacet} onChange={(e) => setUsername(e.target.value)} />
{/* Note: fast-select may not exist, check available components */}
<select value={sortBy !== NO_VALUE ? sortBy : 'name'} onChange={(e) => 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-fiberfor other form components
When unwrapping IS necessary:
- Native
<select>element (iffast-selectdoesn't exist) - Third-party form libraries
- Complex form components that don't have facet equivalents
Best practice: Only unwrap for form controls when no fast-* equivalent exists. Even then, consider wrapping the control in a separate component to limit the scope of re-renders.
Development Workflow
Running Tests
# Run all tests with coverage
yarn test
# Watch mode for development
yarn test:watch
Building Packages
# Build all packages (topological order)
yarn build
# Package for distribution
yarn package
Documentation Site
# Install docs dependencies
yarn docs:install
# Start local docs server
yarn docs:start
# Build docs for production
yarn docs:build
Linting & Formatting
# Format all files
yarn format
# Lint codebase
yarn lint
Testing Patterns
Test File Structure
All test files follow the pattern *.spec.ts or *.spec.tsx and are co-located with source files:
// useFacetMap.spec.tsx
import { render, act } from '@react-facet/dom-fiber-testing-library'
import { createFacet } from '../facet'
import { useFacetMap } from './useFacetMap'
it('maps values from a facet', () => {
const facet = createFacet({ initialValue: 'test' })
const Component = () => {
const mapped = useFacetMap((value) => value.toUpperCase(), [], [facet])
return <fast-text text={mapped} />
}
const { container } = render(<Component />)
expect(container.textContent).toBe('TEST')
act(() => facet.set('hello'))
expect(container.textContent).toBe('HELLO')
})
Key Testing Utilities
render()- Renders components using dom-fiberact()- Wraps facet updates to ensure proper batchingcreateFacet()- Creates mock facets for testing (this is where it's useful!)NO_VALUE- Test initial/uninitialized states
Common Test Patterns
// Testing facet updates
act(() => {
facet.set(newValue)
})
// Testing multiple facets
const facetA = createFacet({ initialValue: 'A' })
const facetB = createFacet({ initialValue: 'B' })
// Testing with NO_VALUE
const facet = createFacet<string>({ initialValue: NO_VALUE })
expect(facet.get()).toBe(NO_VALUE)
Documentation & Resources
- Official Docs: https://react-facet.mojang.com/
- Target Runtime: Coherent Labs Gameface, Chromium Embedded Framework
- Compatibility: Not all React DOM features are ported (no synthetic events layer)
Quick Reference
Most Common Hooks
// State management
useFacetState<T>(initialValue): [Facet<T>, Setter<T>]
// Derivation
useFacetMap<M>(fn, deps, facets, equalityCheck?): Facet<M> // Lightweight, fast init, best default
useFacetMemo<M>(fn, deps, facets, equalityCheck?): Facet<M> // Cached, use for many subscribers/expensive computations
// Side effects
useFacetEffect(effect, deps, facets): void
useFacetLayoutEffect(effect, deps, facets): void
useFacetCallback<M>(callback, deps, facets, defaultReturn?): (...args) => M
// Advanced State Management (NOT RECOMMENDED)
useFacetReducer<S, A>(reducer, initialState, equalityCheck?): [Facet<S>, Dispatch<A>] // Not recommended - use useFacetState instead
useFacetPropSetter<T, Prop>(facet, prop): (value: T[Prop]) => void // Not recommended - use setter callback form instead
// Utilities
useFacetWrap<T>(FacetProp<T>): Facet<T> // Creates new facet on value change
useFacetWrapMemo<T>(FacetProp<T>, equalityCheck?): Facet<T> // Stable facet, updates internal value
useFacetUnwrap<T>(Facet<T>): T (β οΈ causes re-renders!)
useFacetRef<T>(facet): RefObject<T>
// Transitions (React 18+)
useFacetTransition(): [boolean, (fn: () => void) => void] // Hook with pending state
startFacetTransition(fn: () => void): void // Function API for transitions
Core Components
// Conditional mounting
<Mount when={facet}><Child /></Mount>
// List rendering
<Map array={arrayFacet}>{(itemFacet, index) => <Item />}</Map>
// Conditional rendering with value
<With facet={facet}>{(value) => <div>{value}</div>}</With>
// Unwrap facet with controlled re-render scope
<Unwrap data={facet}>{(value) => <ThirdPartyComponent value={value} />}</Unwrap>
// Repeat UI N times based on numeric facet
<Times count={countFacet}>{(index, total) => <div key={index}>Item {index}</div>}</Times>
Facet Factories
// For testing/advanced use only
createFacet<T>({ initialValue, startSubscription, equalityCheck? }): WritableFacet<T>
createStaticFacet<T>(value): Facet<T>
createReadOnlyFacet<T>(facet): Facet<T>
// Context
createFacetContext<T>(): Context<Facet<T>>
Equality Checks
import {
defaultEqualityCheck, // Reference equality (default)
strictEqualityCheck, // Strict equality (===)
shallowObjectEqualityCheck, // Shallow object comparison
shallowArrayEqualityCheck, // Shallow array comparison
} from '@react-facet/core'
Key characteristics:
defaultEqualityCheck: Performance optimized (inlined), primitives use===, objects/arrays always returnfalsestrictEqualityCheck: Type-safe (only primitives & functions), uses===, not optimizedshallowObjectEqualityCheck: Deep value comparison for objects with primitive propertiesshallowArrayEqualityCheck: Deep value comparison for arrays of primitives
Special Values & Types
import { NO_VALUE } from '@react-facet/core'
type Facet<T>
type WritableFacet<T>
type FacetProp<T> = T | Facet<T>
type NoValue = typeof NO_VALUE
type EqualityCheck<T>
Decision Trees & Quick Guides
Which Hook Should I Use?
Creating Facets
Need to create a facet?
ββ Local component state?
β ββ useFacetState(initialValue)
β
ββ Prop that might be value OR facet?
β ββ Value changes frequently?
β β ββ useFacetWrapMemo(prop) // Stable reference
β ββ Otherwise
β ββ useFacetWrap(prop) // Default choice
β
ββ Shared state from context/module?
β ββ useContext(FacetContext)
β
ββ Testing/mocking only?
ββ createFacet({ initialValue })
Deriving Facets
Need to derive from existing facets?
ββ Simple transformation (map, format, calculate)?
β ββ Few subscribers (1-2 components)?
β β ββ useFacetMap(fn, deps, facets) // Default choice
β ββ Many subscribers (3+) OR expensive computation?
β ββ useFacetMemo(fn, deps, facets) // Cached
β
ββ Multiple facets combined?
β ββ useFacetMap(fn, [], [facetA, facetB, ...])
β
ββ With local variables/props?
ββ useFacetMap(fn, [localVar], [facet])
// β οΈ Don't forget first array!
When to Use fast-* Components?
Rendering UI?
ββ Binding a facet to a property?
β ββ β
Use fast-* component
β <fast-div className={classFacet}>
β <fast-text text={messageFacet} />
β </fast-div>
β
ββ Static content only?
β ββ β
Use regular HTML
β <div className="static">
β <p>Static text</p>
β </div>
β
ββ Mix of static and dynamic?
ββ β
Use both together
<div className="container">
<h1>Static Title</h1>
<fast-text text={dynamicFacet} />
</div>
Handling NO_VALUE
Working with facet values?
ββ Using useFacetUnwrap?
β ββ β οΈ CRITICAL: Always check for NO_VALUE
β const value = useFacetUnwrap(facet)
β if (value !== NO_VALUE) {
β // Safe to use value here
β }
β
ββ Setter callback in useFacetState?
β ββ β οΈ CRITICAL: Check before spreading/accessing
β setItems(current =>
β current !== NO_VALUE
β ? [...current, newItem]
β : [newItem]
β )
β
ββ Inside useFacetMap/useFacetMemo?
β ββ β
Automatic - callback receives T, not T | NO_VALUE
β useFacetMap(value => value * 2, [], [facet])
β
ββ Need value in event handler/callback?
ββ β
Use useFacetCallback
const handler = useFacetCallback(
(value) => () => { /* use value */ },
[], [facet]
)
// β οΈ Avoid facet.get() - primarily for testing only
Performance Optimization Decision
Performance issue identified?
ββ Too many re-renders?
β ββ Using useFacetUnwrap for rendering?
β β ββ β REPLACE with fast-text or fast-* component
β β
β ββ Unwrapping for conditional render?
β β ββ β REPLACE with <Mount when={facet}>
β β
β ββ Missing equality check on objects/arrays?
β ββ β
ADD shallowObjectEqualityCheck or shallowArrayEqualityCheck
β
ββ Expensive computation running too often?
β ββ Multiple subscribers to same derivation?
β β ββ β
SWITCH useFacetMap β useFacetMemo
β β
β ββ Heavy updates blocking UI?
β ββ β
WRAP in useFacetTransition or startFacetTransition
β
ββ Missing dependencies causing stale values?
ββ β
ADD to first dependency array
useFacetMap(v => v * multiplier, [multiplier], [facet])
Full Component Examples
Example 1: Game Player Health Bar
Complete component showing multiple concepts:
import { useFacetState, useFacetMap, useFacetEffect, NO_VALUE } from '@react-facet/core'
import { shallowObjectEqualityCheck } from '@react-facet/core'
type PlayerData = {
health: number
maxHealth: number
name: string
}
export const PlayerHealthBar = () => {
// Local state - stable facet reference
const [playerFacet, setPlayer] = useFacetState<PlayerData>({
health: 100,
maxHealth: 100,
name: 'Steve',
})
// Derived facets - lightweight transformations
const healthPercentFacet = useFacetMap((player) => (player.health / player.maxHealth) * 100, [], [playerFacet])
const healthBarClassFacet = useFacetMap(
(percent) => {
if (percent > 66) return 'health-bar-high'
if (percent > 33) return 'health-bar-medium'
return 'health-bar-low'
},
[],
[healthPercentFacet],
)
const healthTextFacet = useFacetMap((player) => `${player.health}/${player.maxHealth}`, [], [playerFacet])
const playerNameFacet = useFacetMap((p) => p.name, [], [playerFacet])
// Side effect - play warning sound when low health
useFacetEffect(
(percent) => {
if (percent < 20) {
console.log('β οΈ Warning: Low health!')
// playWarningSound()
}
},
[],
[healthPercentFacet],
)
// Event handler with NO_VALUE check in setter
const takeDamage = (amount: number) => {
setPlayer((current) => {
if (current === NO_VALUE) return current
return {
...current,
health: Math.max(0, current.health - amount),
}
})
}
const heal = (amount: number) => {
setPlayer((current) => {
if (current === NO_VALUE) return current
return {
...current,
health: Math.min(current.maxHealth, current.health + amount),
}
})
}
return (
<div className="player-health-container">
{/* Static structure */}
<div className="player-info">
<h3>Player</h3>
{/* Dynamic name from facet */}
<fast-text text={playerNameFacet} />
</div>
{/* Health bar with dynamic class */}
<fast-div className={healthBarClassFacet}>
<fast-text text={healthTextFacet} />
</fast-div>
{/* Controls - regular HTML */}
<div className="controls">
<button onClick={() => takeDamage(10)}>Take Damage (-10)</button>
<button onClick={() => heal(10)}>Heal (+10)</button>
</div>
</div>
)
}
Example 2: Inventory List with Conditional Rendering
Shows Map, Mount, and With components:
import { useFacetState, useFacetMap, useFacetCallback, Facet, NO_VALUE } from '@react-facet/core'
import { Map, Mount, With } from '@react-facet/core'
import { shallowArrayEqualityCheck } from '@react-facet/core'
type Item = {
id: string
name: string
quantity: number
}
export const Inventory = () => {
const [itemsFacet, setItems] = useFacetState<Item[]>([
{ id: '1', name: 'Diamond', quantity: 5 },
{ id: '2', name: 'Gold', quantity: 12 },
])
const [selectedIdFacet, setSelectedId] = useFacetState<string | null>(null)
// Derived facet with equality check for arrays
const sortedItemsFacet = useFacetMap(
(items) => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[],
[itemsFacet],
shallowArrayEqualityCheck,
)
const hasItemsFacet = useFacetMap((items) => items.length > 0, [], [itemsFacet])
const hasNoItemsFacet = useFacetMap((has) => !has, [], [hasItemsFacet])
// Regular callback - no need for useFacetCallback since setter gives us current value
const addItem = (name: string) => {
setItems((current) =>
current !== NO_VALUE
? [...current, { id: Date.now().toString(), name, quantity: 1 }]
: [{ id: Date.now().toString(), name, quantity: 1 }],
)
}
return (
<div className="inventory">
<h2>Inventory</h2>
{/* Conditional rendering with Mount */}
<Mount when={hasItemsFacet}>
<div className="item-list">
{/* List rendering with Map */}
<Map array={sortedItemsFacet}>
{(itemFacet, index) => <InventoryItem key={index} itemFacet={itemFacet} />}
</Map>
</div>
</Mount>
{/* Show message when empty - opposite condition */}
<Mount when={hasNoItemsFacet}>
<p>No items in inventory</p>
</Mount>
{/* With component for conditional rendering with value access */}
<With facet={selectedIdFacet}>{(selectedId) => selectedId && <div>Selected: {selectedId}</div>}</With>
<button onClick={() => addItem('Iron')}>Add Iron</button>
</div>
)
}
// Separate component to use hooks properly (React Rules of Hooks)
const InventoryItem = ({ itemFacet }: { itemFacet: Facet<Item> }) => {
const nameFacet = useFacetMap((item) => item.name, [], [itemFacet])
const quantityFacet = useFacetMap((item) => item.quantity, [], [itemFacet])
return (
<div className="item">
<fast-text text={nameFacet} />
<span> x </span>
<fast-text text={quantityFacet} />
</div>
)
}
Example 3: Form with Transitions
Heavy computation during form submission:
import { useFacetState, useFacetMap, useFacetCallback, useFacetTransition, NO_VALUE } from '@react-facet/core'
import { shallowObjectEqualityCheck } from '@react-facet/core'
type FormData = {
username: string
email: string
}
export const UserForm = () => {
const [formDataFacet, setFormData] = useFacetState<FormData>({
username: '',
email: '',
})
const [resultsFacet, setResults] = useFacetState<string[]>([])
const [isPending, startTransition] = useFacetTransition()
// Validation (runs immediately, high priority)
const isValidFacet = useFacetMap((data) => data.username.length >= 3 && data.email.includes('@'), [], [formDataFacet])
// Unwrap validation for use with regular button element
const isValid = useFacetUnwrap(isValidFacet)
const hasResultsFacet = useFacetMap((r) => r.length > 0, [], [resultsFacet])
// Update handlers with NO_VALUE checks
const updateUsername = (username: string) => {
setFormData((current) => (current !== NO_VALUE ? { ...current, username } : { username, email: '' }))
}
const updateEmail = (email: string) => {
setFormData((current) => (current !== NO_VALUE ? { ...current, email } : { username: '', email }))
}
// Heavy computation wrapped in transition
const handleSubmit = useFacetCallback(
(data) => () => {
startTransition(() => {
try {
// Simulate expensive validation/processing
const processed = heavyDataProcessing(data)
setResults(processed)
} catch (error) {
console.error('Submission failed:', error)
setResults(['Error processing form'])
}
})
},
[],
[formDataFacet],
)
return (
<div className="user-form">
<h2>User Registration</h2>
{/* Input fields - always responsive */}
<div className="form-field">
<label>Username:</label>
<input type="text" onChange={(e) => updateUsername(e.target.value)} placeholder="Enter username" />
</div>
<div className="form-field">
<label>Email:</label>
<input type="text" onChange={(e) => updateEmail(e.target.value)} placeholder="Enter email" />
</div>
{/* Submit button with validation - unwrap facet for non-facet-aware button */}
<button onClick={handleSubmit} disabled={isValid === NO_VALUE || !isValid}>
{isPending ? 'Processing...' : 'Submit'}
</button>
{/* Results rendered with facet */}
<Mount when={hasResultsFacet}>
<div className="results">
<Map array={resultsFacet}>
{(resultFacet, index) => (
<div key={index}>
<fast-text text={resultFacet} />
</div>
)}
</Map>
</div>
</Mount>
</div>
)
}
// 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:
- CRITICAL: Always check for
NO_VALUEafteruseFacetUnwrap- the return type isT | NO_VALUE, notT - CRITICAL: Always check for
NO_VALUEinuseFacetStatesetter callbacks - the previous value isOption<T>(T | NO_VALUE), not justT - CRITICAL: Avoid
useFacetUnwrapunless absolutely necessary (causes re-renders!) - CRITICAL: Use
Mountfor conditional rendering, NEVERuseFacetUnwrap- unwrapping defeats the entire purpose of facets - CRITICAL: Use
createRoot(notrender) for mounting -renderis deprecated - CRITICAL: NEVER use
facet.get()in application code - it's for testing only; useuseFacetCallbackinstead - CRITICAL: Use
fast-inputinstead of unwrapping for<input>- unwrapping causes re-renders - CRITICAL: Use
fast-*components ONLY when binding facets to attributes - use regular HTML for static content - Always use facet naming convention (
*Facetsuffix for variables) - Use
useFacetStateoruseFacetWrapfor creating facets - avoidcreateFacetin application code - Use TWO dependency arrays correctly:
- First array: non-facet dependencies (props, local vars)
- Second array: facet dependencies
- Default to
useFacetMapfor derivations - only useuseFacetMemowhen you have many subscribers or expensive computations - Use
useFacetWrapvsuseFacetWrapMemoappropriately:
useFacetWrap: Default choice, creates new facet on value changeuseFacetWrapMemo: When you need stable facet references or wrap frequently changing props
- Use transitions for heavy updates:
useFacetTransition: In components when you need pending statestartFacetTransition: Outside components or when pending state not needed- The
startTransitioncallback fromuseFacetTransitionis stable - don't include it in dependency arrays - Always wrap risky computations in try-catch blocks within transitions
- Add equality checks for object/array derivations to prevent unnecessary updates
- Handle
NO_VALUEin facet operations where appropriate - Understand NO_VALUE retention behavior:
- When a mapping function (
useFacetMap/useFacetMemo) returnsNO_VALUE, the derived facet retains its previous value (doesn't notify listeners) - When a setter callback (
useFacetState) returnsNO_VALUE, the facet retains its previous value (doesn't notify listeners) - Useful for conditional updates, validation, and clamping values
- When a mapping function (
- Use
typeinstead ofinterfacefor TypeScript definitions - Don't use
batchin application code - it's for internal library use - Never call hooks inside conditionals, loops, or nested functions (like Map callbacks) - violates Rules of Hooks
- Best practice: Define derived facets at component top level for clarity and stable references
- Test facet-based components using
@react-facet/dom-fiber-testing-library - Understand facet reference stability:
useFacetState: Facet reference is stable - never changes across re-rendersuseFacetMap/useFacetMemo: Create new facet reference when dependencies change (non-facet deps, facets array, or equalityCheck)useFacetWrap: Creates new facet reference when wrapped value changesuseFacetWrapMemo: Maintains stable facet reference, updates value internally
When reviewing React Facet code, check for:
- Mount for conditional rendering: NEVER use
useFacetUnwrapfor conditional rendering (CRITICAL) - createRoot for mounting: NEVER use deprecated
rendermethod (CRITICAL) - NO
facet.get()in application code: CRITICAL - useuseFacetCallbackinstead (CRITICAL) - Use
fast-inputfor form inputs: NEVER unwrap facets for<input>whenfast-inputexists (CRITICAL) fast-*only for facet bindings: NEVER usefast-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:
useFacetWrapvsuseFacetWrapMemobased on stability needs - Transitions for heavy updates: Using
useFacetTransitionorstartFacetTransitionfor expensive operations - Error handling in transitions: Try-catch blocks wrapping risky computations
- Minimal use of
useFacetUnwrap(red flag if used frequently, especially for<input>) NO_VALUEchecks afteruseFacetUnwrap(CRITICAL - must check before using unwrapped values)NO_VALUEchecks inuseFacetStatesetter callbacks (CRITICAL - must check before spreading/accessing properties)- NO_VALUE retention awareness: Understanding that returning
NO_VALUEfrom mappers/setters retains previous value - Regular HTML for static content: Use
<div>,<span>, etc. when no facet bindings needed createFacetonly 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)
typeoverinterfacein TypeScript definitionsuseFacetMapas default for derivations -useFacetMemoonly when needed for performance- Stable callback awareness: Not including
startTransitionfromuseFacetTransitionin 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.shto 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:
// β
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 (
<fast-div className={classFacet}>
<fast-text text={textFacet} />
<fast-input value={textFacet} />
</fast-div>
)
}
// β WRONG - Missing renderer import causes TypeScript errors
// @esModuleInterop
import { useFacetState } from '@react-facet/core'
const Example = () => {
const [textFacet] = useFacetState('Hello')
return (
<fast-div>
{' '}
{/* Error: Property 'fast-div' does not exist */}
<fast-text text={textFacet} /> {/* Error: Property 'fast-text' does not exist */}
</fast-div>
)
}
The pattern:
- Add
// @esModuleInteropat 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:
// β
Works without renderer import
// @esModuleInterop
import { useFacetState } from '@react-facet/core'
const Example = () => {
return <div>Static content</div> // 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
Trust
Not scanned yet. Artifacts are graded after they are crawled, so a recently discovered one may have no result for a while.
Versions
git-9cd7b496b3d72026-08-04