State Management & Persistence

UI Builder provides flexible state management and persistence options for your layouts and variables. Choose between automatic local storage, custom database integration, or complete manual control based on your application's needs.

Understanding UI Builder State

UI Builder manages two main types of state:

  • Pages & Layers: The component hierarchy, structure, and configuration
  • Variables: Dynamic data definitions that can be bound to component properties

Both are managed independently and can be persisted using different strategies.

Local Storage Persistence

By default, UI Builder automatically saves state to browser local storage:

tsx
1// Default behavior - auto-saves to localStorage 2<UIBuilder componentRegistry={componentRegistry} /> 3 4// Disable local storage persistence 5<UIBuilder 6 componentRegistry={componentRegistry} 7 persistLayerStore={false} 8/>

When to use: Development, prototyping, or single-user applications where browser storage is sufficient.

Limitations: Data is tied to the browser/device and can be cleared by the user.

Database Integration

For production applications, integrate with your database using the initialization and callback props:

tsx
1import UIBuilder from '@/components/ui/ui-builder'; 2import type { ComponentLayer, Variable, LayerChangeHandler, VariableChangeHandler } from '@/components/ui/ui-builder/types'; 3 4function DatabaseIntegratedBuilder({ userId }: { userId: string }) { 5 const [initialLayers, setInitialLayers] = useState<ComponentLayer[]>(); 6 const [initialVariables, setInitialVariables] = useState<Variable[]>(); 7 const [isLoading, setIsLoading] = useState(true); 8 9 // Load initial state from database 10 useEffect(() => { 11 async function loadUserLayout() { 12 try { 13 const [layoutRes, variablesRes] = await Promise.all([ 14 fetch(`/api/layouts/${userId}`), 15 fetch(`/api/variables/${userId}`) 16 ]); 17 18 const layoutData = await layoutRes.json(); 19 const variablesData = await variablesRes.json(); 20 21 setInitialLayers(layoutData.layers || []); 22 setInitialVariables(variablesData.variables || []); 23 } catch (error) { 24 console.error('Failed to load layout:', error); 25 setInitialLayers([]); 26 setInitialVariables([]); 27 } finally { 28 setIsLoading(false); 29 } 30 } 31 32 loadUserLayout(); 33 }, [userId]); 34 35 // Save layers to database 36 const handleLayersChange: LayerChangeHandler = async (updatedLayers) => { 37 try { 38 await fetch(`/api/layouts/${userId}`, { 39 method: 'PUT', 40 headers: { 'Content-Type': 'application/json' }, 41 body: JSON.stringify({ layers: updatedLayers }) 42 }); 43 } catch (error) { 44 console.error('Failed to save layers:', error); 45 } 46 }; 47 48 // Save variables to database 49 const handleVariablesChange: VariableChangeHandler = async (updatedVariables) => { 50 try { 51 await fetch(`/api/variables/${userId}`, { 52 method: 'PUT', 53 headers: { 'Content-Type': 'application/json' }, 54 body: JSON.stringify({ variables: updatedVariables }) 55 }); 56 } catch (error) { 57 console.error('Failed to save variables:', error); 58 } 59 }; 60 61 if (isLoading) { 62 return <div>Loading your layout...</div>; 63 } 64 65 return ( 66 <UIBuilder 67 componentRegistry={componentRegistry} 68 initialLayers={initialLayers} 69 onChange={handleLayersChange} 70 initialVariables={initialVariables} 71 onVariablesChange={handleVariablesChange} 72 persistLayerStore={false} // Disable localStorage 73 /> 74 ); 75}

Persistence-Related Props

Core Persistence Props

  • persistLayerStore (boolean, default: true): Controls localStorage persistence
  • initialLayers (ComponentLayer[]): Initial pages/layers to load
  • onChange (LayerChangeHandler<TRegistry>): Callback when layers change
  • initialVariables (Variable[]): Initial variables to load
  • onVariablesChange (VariableChangeHandler): Callback when variables change

Permission Control Props

Control what users can modify to prevent unwanted changes:

  • allowVariableEditing (boolean, default: true): Allow variable creation/editing
  • allowPagesCreation (boolean, default: true): Allow new page creation
  • allowPagesDeletion (boolean, default: true): Allow page deletion
tsx
1// Read-only editor for content review 2<UIBuilder 3 componentRegistry={componentRegistry} 4 initialLayers={existingLayout} 5 allowVariableEditing={false} 6 allowPagesCreation={false} 7 allowPagesDeletion={false} 8 persistLayerStore={false} 9/>

Working Examples

Explore these working examples in the project:

The examples demonstrate different persistence patterns you can adapt for your use case.

Debounced Auto-Save

Avoid excessive API calls with debounced saving:

tsx
1import { useCallback } from 'react'; 2import { debounce } from 'lodash'; 3 4function AutoSaveBuilder() { 5 // Debounce saves to reduce API calls 6 const debouncedSaveLayers = useCallback( 7 debounce(async (layers: ComponentLayer[]) => { 8 try { 9 await fetch('/api/layouts/save', { 10 method: 'POST', 11 headers: { 'Content-Type': 'application/json' }, 12 body: JSON.stringify({ layers }) 13 }); 14 } catch (error) { 15 console.error('Auto-save failed:', error); 16 } 17 }, 2000), // 2 second delay 18 [] 19 ); 20 21 const debouncedSaveVariables = useCallback( 22 debounce(async (variables: Variable[]) => { 23 try { 24 await fetch('/api/variables/save', { 25 method: 'POST', 26 headers: { 'Content-Type': 'application/json' }, 27 body: JSON.stringify({ variables }) 28 }); 29 } catch (error) { 30 console.error('Variables auto-save failed:', error); 31 } 32 }, 2000), 33 [] 34 ); 35 36 return ( 37 <UIBuilder 38 componentRegistry={componentRegistry} 39 onChange={debouncedSaveLayers} 40 onVariablesChange={debouncedSaveVariables} 41 persistLayerStore={false} 42 /> 43 ); 44}

Data Format

UI Builder saves data as plain JSON with a predictable structure:

json
1{ 2 "layers": [ 3 { 4 "id": "page-1", 5 "type": "div", 6 "name": "Page 1", 7 "props": { 8 "className": "p-4 bg-white" 9 }, 10 "children": [ 11 { 12 "id": "button-1", 13 "type": "Button", 14 "name": "Submit Button", 15 "props": { 16 "variant": "default", 17 "children": { 18 "__variableRef": "buttonText" 19 } 20 }, 21 "children": [] 22 } 23 ] 24 } 25 ], 26 "variables": [ 27 { 28 "id": "buttonText", 29 "name": "Button Text", 30 "type": "string", 31 "defaultValue": "Click Me!" 32 } 33 ] 34}

Variable References: Component properties bound to variables use { "__variableRef": "variableId" } format.

Next.js API Route Examples

Layout API Route

tsx
1// app/api/layouts/[userId]/route.ts 2import { NextRequest, NextResponse } from 'next/server'; 3 4export async function GET( 5 request: NextRequest, 6 { params }: { params: { userId: string } } 7) { 8 try { 9 const layout = await getUserLayout(params.userId); 10 return NextResponse.json({ layers: layout }); 11 } catch (error) { 12 return NextResponse.json( 13 { error: 'Failed to load layout' }, 14 { status: 500 } 15 ); 16 } 17} 18 19export async function PUT( 20 request: NextRequest, 21 { params }: { params: { userId: string } } 22) { 23 try { 24 const { layers } = await request.json(); 25 await saveUserLayout(params.userId, layers); 26 return NextResponse.json({ success: true }); 27 } catch (error) { 28 return NextResponse.json( 29 { error: 'Failed to save layout' }, 30 { status: 500 } 31 ); 32 } 33}

Variables API Route

tsx
1// app/api/variables/[userId]/route.ts 2export async function PUT( 3 request: NextRequest, 4 { params }: { params: { userId: string } } 5) { 6 try { 7 const { variables } = await request.json(); 8 await saveUserVariables(params.userId, variables); 9 return NextResponse.json({ success: true }); 10 } catch (error) { 11 return NextResponse.json( 12 { error: 'Failed to save variables' }, 13 { status: 500 } 14 ); 15 } 16}

Error Handling & Recovery

Implement robust error handling for production applications:

tsx
1function RobustBuilder() { 2 const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'error' | 'success'>('idle'); 3 const [lastError, setLastError] = useState<string | null>(null); 4 5 const handleSaveWithRetry = async (layers: ComponentLayer[], retries = 3) => { 6 setSaveStatus('saving'); 7 8 for (let i = 0; i < retries; i++) { 9 try { 10 await fetch('/api/layouts/save', { 11 method: 'POST', 12 headers: { 'Content-Type': 'application/json' }, 13 body: JSON.stringify({ layers }) 14 }); 15 16 setSaveStatus('success'); 17 setLastError(null); 18 return; 19 } catch (error) { 20 if (i === retries - 1) { 21 setSaveStatus('error'); 22 setLastError('Failed to save after multiple attempts'); 23 } else { 24 // Wait before retry with exponential backoff 25 await new Promise(resolve => 26 setTimeout(resolve, Math.pow(2, i) * 1000) 27 ); 28 } 29 } 30 } 31 }; 32 33 return ( 34 <div className="flex flex-col h-full"> 35 {/* Status Bar */} 36 <div className="flex items-center justify-between p-2 border-b bg-muted/50"> 37 <div className="flex items-center gap-2"> 38 {saveStatus === 'saving' && ( 39 <Badge variant="outline">Saving...</Badge> 40 )} 41 {saveStatus === 'success' && ( 42 <Badge variant="default">Saved</Badge> 43 )} 44 {saveStatus === 'error' && ( 45 <Badge variant="destructive">Save Failed</Badge> 46 )} 47 </div> 48 49 {lastError && ( 50 <Button 51 variant="outline" 52 size="sm" 53 onClick={() => window.location.reload()} 54 > 55 Reload Page 56 </Button> 57 )} 58 </div> 59 60 {/* Builder */} 61 <div className="flex-1"> 62 <UIBuilder 63 componentRegistry={componentRegistry} 64 onChange={handleSaveWithRetry} 65 persistLayerStore={false} 66 /> 67 </div> 68 </div> 69 ); 70}

Best Practices

1. Choose the Right Persistence Strategy

  • localStorage: Development, prototyping, single-user apps
  • Database: Production, multi-user, collaborative editing
  • Hybrid: localStorage for drafts, database for published versions

2. Implement Proper Error Handling

  • Always handle save failures gracefully
  • Provide user feedback for save status
  • Implement retry logic with exponential backoff
  • Offer recovery options when saves fail

3. Optimize API Performance

  • Use debouncing to reduce API calls (2-5 second delays)
  • Consider batching layer and variable saves
  • Implement optimistic updates for better UX
  • Use proper HTTP status codes and error responses

4. Control User Permissions

  • Use allowVariableEditing={false} for content-only editing
  • Set allowPagesCreation={false} for fixed page structures
  • Implement role-based access control in your API routes

5. Plan for Scale

  • Consider data size limits (layers can grow large)
  • Implement pagination for large datasets
  • Use database indexing on user IDs and timestamps
  • Consider caching frequently accessed layouts