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.
UI Builder manages two main types of state:
Both are managed independently and can be persisted using different strategies.
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.
Explore these working examples in the project:
allowPagesCreation={false} and allowPagesDeletion={false}The examples demonstrate different persistence patterns you can adapt for your use case.
allowVariableEditing={false} for content-only editingallowPagesCreation={false} for fixed page structuresFor 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}
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 changeControl 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 deletiontsx1// 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/>
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}
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.
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}
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}
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}