useFacetState
To define facets within React components, there is a main hook useFacetState that provides a very familiar API when compared with React's useState.
Returns a [facet, setFacet] pair. Like React's useState, but with a Facet instead of a value.
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.
This makes useFacetState perfect for creating persistent state that can be safely passed to child components without causing unnecessary re-renders from reference changes.
This example illustrates how to use this hook in the common use case of having to store the temporary state of the input field until it is submitted.
tsxconstForm = ({onSubmit ,initialValue }:Props ) => {const [value ,setValue ] =useFacetState (initialValue )consthandleChange =useCallback <KeyboardCallback >((event ) => {if (event .target instanceofHTMLInputElement ) {setValue (event .target .value )}},[setValue ],)consthandleClick =useFacetCallback ((currentValue ) => () => {onSubmit (currentValue )},[onSubmit ],[value ],)return (<fast-div ><fast-input onKeyUp ={handleChange }value ={value } /><fast-div onClick ={handleClick }>Submit</fast-div ></fast-div >)}
Handling Previous Values with NO_VALUE
When using the functional form of the setter (callback), the previous value is Option<T> (i.e., T | NO_VALUE), not just T. You must check for NO_VALUE before using the previous value to avoid runtime errors.
tsxconst [itemsFacet, setItems] = useFacetState<string[]>([])// ❌ WRONG - current might be NO_VALUE, can't spread a Symbol!setItems((current) => [...current, newItem])// ✅ CORRECT - Check for NO_VALUE firstsetItems((current) => (current !== NO_VALUE ? [...current, newItem] : [newItem]))
This is exactly the same requirement as useFacetUnwrap - anytime you access a facet's value (directly or through a callback), it might be NO_VALUE.
NO_VALUE Retention Behavior in Setters
When a setter callback returns NO_VALUE, the facet retains its previous value rather than updating to NO_VALUE. The facet's internal value is set to NO_VALUE, but listeners are not notified, so subscribers continue seeing the last emitted value.
This is useful for conditional updates where you want to prevent state changes under certain conditions:
tsximport {useFacetState ,NO_VALUE } from '@react-facet/core'constConditionalCounter = () => {const [countFacet ,setCount ] =useFacetState (0)constincrementIfBelow5 = () => {setCount ((current ) => {if (current ===NO_VALUE ) return 1if (current >= 5) returnNO_VALUE // Prevent updates once we hit 5returncurrent + 1})}// countFacet will update: 0 → 1 → 2 → 3 → 4 → 5 → (stays 5)// Clicks after 5 don't trigger updates because we return NO_VALUEreturn <button onClick ={incrementIfBelow5 }>Increment (max 5)</button >}
Key points:
- Returning
NO_VALUEfrom a setter callback does not propagate to subscribers - It prevents the facet from updating, keeping the last emitted value for all observers
- The internal state becomes
NO_VALUE, but listeners aren't called - Useful for implementing validation, conditional updates, or preventing unwanted state changes
Common Patterns
When updating state based on the previous value, always check for NO_VALUE:
tsximport {useFacetState ,NO_VALUE } from '@react-facet/core'constTodoList = () => {const [todosFacet ,setTodos ] =useFacetState <string[]>([])constaddTodo = (todo : string) => {// ✅ CORRECT - Always check for NO_VALUE when using callback formsetTodos ((current ) => (current !==NO_VALUE ? [...current ,todo ] : [todo ]))}constremoveTodo = (index : number) => {// ✅ CORRECT - Check before filteringsetTodos ((current ) => (current !==NO_VALUE ?current .filter ((_ ,i ) =>i !==index ) : []))}return <div >Todo List</div >}
For objects, the same pattern applies:
tsximport {useFacetState ,NO_VALUE } from '@react-facet/core'typeUser = {name : string;age : number }constUserProfile = () => {const [userFacet ,setUser ] =useFacetState <User >({name : 'Alice',age : 30 })constupdateName = (newName : string) => {// ✅ CORRECT - Check before spreadingsetUser ((current ) => (current !==NO_VALUE ? { ...current ,name :newName } : {name :newName ,age : 0 }))}return <div >User Profile</div >}
Common patterns:
tsximport {useFacetState ,NO_VALUE } from '@react-facet/core'constExamples = () => {const [countFacet ,setCount ] =useFacetState (0)const [itemsFacet ,setItems ] =useFacetState <string[]>([])// Incrementing a numberconstincrement = () => {setCount ((current ) => (current !==NO_VALUE ?current + 1 : 1))}// Appending to arrayconstappendItem = (item : string) => {setItems ((current ) => (current !==NO_VALUE ? [...current ,item ] : [item ]))}// Replacing array elementconstreplaceItem = (index : number,newItem : string) => {setItems ((current ) => (current !==NO_VALUE ?current .map ((item ,i ) => (i ===index ?newItem :item )) : [newItem ]))}return <div >Examples</div >}