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:
tsx1// 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:
tsx1import 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 persistenceinitialLayers
(ComponentLayer[]
): Initial pages/layers to loadonChange
(LayerChangeHandler<TRegistry>
): Callback when layers changeinitialVariables
(Variable[]
): Initial variables to loadonVariablesChange
(VariableChangeHandler
): Callback when variables change
Permission Control Props
Control what users can modify to prevent unwanted changes:
allowVariableEditing
(boolean
, default:true
): Allow variable creation/editingallowPagesCreation
(boolean
, default:true
): Allow new page creationallowPagesDeletion
(boolean
, default:true
): Allow page deletion
tsx1// 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:
- Basic Example: Simple setup with localStorage persistence
- Editor Example: Full editor with custom configuration
- Immutable Pages: Read-only pages with
allowPagesCreation={false}
andallowPagesDeletion={false}
The examples demonstrate different persistence patterns you can adapt for your use case.
Debounced Auto-Save
Avoid excessive API calls with debounced saving:
tsx1import { 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:
json1{ 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
tsx1// 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
tsx1// 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:
tsx1function 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