useFacetTransition
React Facet provides APIs for integrating facet updates with React 18's concurrent rendering features. These allow you to mark facet updates as non-urgent transitions, enabling React to keep the UI responsive during heavy updates.
The Hook: useFacetTransition
A hook that works analogously to React's useTransition, but ensures that any React state changes resulting from facet updates are handled within a React transition.
Hook Signature
typescriptfunction useFacetTransition(): [boolean, (fn: () => void) => void]
Returns: A tuple containing:
isPending- Boolean indicating if a transition is in progressstartTransition- Function to execute facet updates as a transition
The startTransition function returned by useFacetTransition is stable across re-renders and doesn't need to be included in dependency arrays of useCallback, useEffect, or other hooks.
When to Use
Use useFacetTransition when:
- Heavy facet updates - Facet changes trigger expensive computations or rendering
- Keeping UI responsive - Need to prioritize user interactions over state updates
- Large lists or complex UIs - Updates affect many components simultaneously
- Mixed React state and facets - Some components use React state alongside facets
Basic Usage
The following example keeps input fields responsive while processing updates, during which time it indicates to the user that it is loading via the isPending flag:
tsximport {useFacetState ,useFacetTransition } from '@react-facet/core'constSearchResults = () => {const [queryFacet ,setQuery ] =useFacetState ('')const [resultsFacet ,setResults ] =useFacetState <string[]>([])const [isPending ,startTransition ] =useFacetTransition ()consthandleSearch = (newQuery : string) => {// Update query immediately (high priority)setQuery (newQuery )// Update results as a transition (low priority)startTransition (() => {// Expensive computation or data fetchingconstresults =performExpensiveSearch (newQuery )setResults (results )})}return (<div ><input type ="text"onChange ={(e ) =>handleSearch (e .target .value )}placeholder ="Search..." />{isPending && <div >Searching...</div >}<fast-div style ={{opacity :isPending ? 0.5 : 1 }}>{/* Results display */}</fast-div ></div >)}constperformExpensiveSearch = (query : string): string[] => {// Simulate expensive searchreturnArray .from ({length : 1000 }, (_ ,i ) => `Result ${i } for ${query }`)}
The Function: startFacetTransition
A function API that works analogously to React's startTransition, for use outside of components.
Function Signature
typescriptfunction startFacetTransition(fn: () => void): void
Parameters:
fn- Function containing facet updates to execute as a transition
When to Use startFacetTransition
Use startFacetTransition when:
- Outside React components - In utility functions, event handlers, or callbacks
- Global state updates - Updating shared facets from non-React code
- One-off transitions - Don't need the
isPendingstate - Event handlers - Processing events that trigger heavy facet updates
Usage Examples
tsximport {useFacetState ,startFacetTransition } from '@react-facet/core'// Utility function outside of Reactexport constloadDataAsTransition = (setData : (data : string[]) => void,newData : string[]) => {startFacetTransition (() => {// Heavy update marked as low prioritysetData (newData )})}// Use in componentconstComponent = () => {const [dataFacet ,setData ] =useFacetState <string[]>([])consthandleLoad = () => {constdata =Array .from ({length : 5000 }, (_ ,i ) => `Item ${i }`)loadDataAsTransition (setData ,data )}return <button onClick ={handleLoad }>Load Data</button >}
In Event Handlers
tsximport {useFacetState ,startFacetTransition } from '@react-facet/core'constBatchProcessor = () => {const [statusFacet ,setStatus ] =useFacetState ('Ready')constprocessBatch = (items : string[]) => {setStatus ('Processing...')// Process as transition - won't block UIstartFacetTransition (() => {items .forEach ((item ) => {// Heavy processing per itemprocessItem (item )})setStatus ('Complete')})}return (<div ><button onClick ={() =>processBatch (generateItems ())}>Process Batch</button ><fast-text text ={statusFacet } /></div >)}constgenerateItems = () =>Array .from ({length : 1000 }, (_ ,i ) => `Item ${i }`)constprocessItem = (item : string) => {/* heavy processing */}
With Shared State
When working with shared state across multiple components, startFacetTransition (the function API) is often preferable to useFacetTransition (the hook):
Why use startFacetTransition for shared state:
- No pending state needed - Notifications are "fire-and-forget", consumers don't need loading indicators
- Called from children - The transition is triggered in child components, not where
isPendingwould be available - Cleaner API - No need to expose
isPendingthrough context if it won't be used - Better performance - Provider doesn't re-render when
isPendingchanges
Comparison:
tsximport {useFacetState ,useFacetTransition ,startFacetTransition ,NO_VALUE } from '@react-facet/core'import {createContext ,useContext } from 'react'import type {Facet } from '@react-facet/core'// ❌ Less ideal: Using useFacetTransition in provider causes re-renderstypeNotificationContextBad = {notificationsFacet :Facet <string[]>addNotification : (message : string) => voidisPending : boolean // Provider re-renders when this changes}constNotificationContextBad =createContext <NotificationContextBad | null>(null)export constNotificationProviderBad = ({children }: {children :React .ReactNode }) => {const [notificationsFacet ,setNotifications ] =useFacetState <string[]>([])const [isPending ,startTransition ] =useFacetTransition ()constaddNotification = (message : string) => {startTransition (() => {setNotifications ((current ) => (current !==NO_VALUE ? [...current ,message ] : [message ]))})}// ⚠️ Provider re-renders on every isPending change, even if not usedreturn (<NotificationContextBad .Provider value ={{notificationsFacet ,addNotification ,isPending }}>{children }</NotificationContextBad .Provider >)}// ✅ Better: Using startFacetTransition avoids unnecessary re-renderstypeNotificationContextGood = {notificationsFacet :Facet <string[]>addNotification : (message : string) => void}constNotificationContextGood =createContext <NotificationContextGood | null>(null)export constNotificationProviderGood = ({children }: {children :React .ReactNode }) => {const [notificationsFacet ,setNotifications ] =useFacetState <string[]>([])constaddNotification = (message : string) => {// ✅ Use startFacetTransition: no isPending state, no re-rendersstartFacetTransition (() => {setNotifications ((current ) => (current !==NO_VALUE ? [...current ,message ] : [message ]))})}return (<NotificationContextGood .Provider value ={{notificationsFacet ,addNotification }}>{children }</NotificationContextGood .Provider >)}
How Transitions Work
When you use useFacetTransition or startFacetTransition:
- Batching - Facet updates are batched together using
batchTransitioninternally (a special variant that ensures all tasks run at the end of the transition, maintaining separate task queues for transition and non-transition updates) - Priority - React treats these updates as low priority (interruptible)
- Concurrent rendering - React can pause and resume the work
- User responsiveness - High-priority updates (like user input) can interrupt transitions
- Task Queue Separation - Transition tasks are queued separately from regular tasks, ensuring proper priority ordering and preventing transition updates from blocking urgent updates
Nested Transitions
Transitions can be nested within each other:
- Inner transitions inherit the transition context from outer transitions
- Task queues are maintained separately for transition and non-transition contexts
- Tasks flush when the outermost transition completes
- This allows for complex update patterns while maintaining UI responsiveness
tsximport {useFacetState ,startFacetTransition } from '@react-facet/core'constComplexUpdate = () => {const [dataFacet ,setData ] =useFacetState <string[]>([])consthandleUpdate = () => {// Outer transitionstartFacetTransition (() => {constpartialData =processFirstBatch ()setData (partialData )// Inner transition - will complete when outer completesstartFacetTransition (() => {constfinalData =processSecondBatch ()setData (finalData )})})}return <button onClick ={handleUpdate }>Update</button >}constprocessFirstBatch = () => ['item1', 'item2']constprocessSecondBatch = () => ['item1', 'item2', 'item3']
Error Handling
If a facet update throws an error within a transition, the behavior is:
- All remaining queued tasks in the current batch are cancelled
- The task queue is cleared to prevent cascading errors
- The error is re-thrown for you to handle
Always ensure your facet updates handle errors appropriately:
tsximport {useFacetState ,startFacetTransition } from '@react-facet/core'constSafeUpdate = () => {const [dataFacet ,setData ] =useFacetState <string[]>([])const [errorFacet ,setError ] =useFacetState <string | null>(null)consthandleLoad = () => {startFacetTransition (() => {try {constresult =riskyComputation ()setData (result )setError (null)} catch (error ) {setError (error instanceofError ?error .message : 'Unknown error')}})}return <button onClick ={handleLoad }>Load Data</button >}constriskyComputation = (): string[] => {if (Math .random () > 0.5) throw newError ('Computation failed')return ['data']}
Performance Benefits
Without transitions:
tsx// Heavy update blocks the UIsetData(expensiveComputation())// User interactions are delayed until this completes
With transitions:
tsxstartTransition(() => {setData(expensiveComputation())})// User interactions remain responsive// Heavy update happens in the background
Best Practices
- Don't transition urgent updates - Keep critical UI feedback (like input fields) outside transitions
- Show pending state - Use
isPendingto indicate background work when usinguseFacetTransition - Handle errors explicitly - Always wrap risky computations in try-catch blocks within transitions
- Combine with useFacetMemo - Cache expensive derivations within transitions for better performance
- Test on slower devices - Transitions shine on lower-end hardware
- Use
startFacetTransitionfor shared state - Avoid unnecessary provider re-renders whenisPendingisn't needed - Don't include
startTransitionin dependency arrays - The callback fromuseFacetTransitionis stable
Comparison: Hook vs Function API
| Feature | useFacetTransition | startFacetTransition |
|---|---|---|
| Usage location | Inside React components | Anywhere (components or utils) |
| Returns pending state | Yes (isPending) | No |
| Re-renders on pending | Yes | N/A |
| Callback stability | Stable (safe in deps arrays) | N/A |
| Best for | Interactive UI with feedback | Fire-and-forget updates |
| Example use case | Search with loading spinner | Background data refresh |
| Error handling | Errors cancel remaining tasks and re-throw | Errors cancel remaining tasks and re-throw |
Use transitions for updates that affect large portions of your UI or trigger expensive computations. This keeps your app feeling snappy even during heavy updates.