I am trying to build a form-builder using Next.js, but I encounter this issue when trying to create a LivePreview. I am using a three-column layout (sidebar, canvas, LivePreview). It is supposed to be an open-source form-builder website, where people can create a form and share a link to the form.
The problem is that when I drag a field and drop on the canvas, the whole website freezes, I have isolated the problem to the LivePreview component, but I still do not know what the problem is.
I will add my folder structure. I am using a third-party package called react-window to try and memoize my array when mapping
I
## Error Type
Runtime TypeError
## Error Message
Cannot convert undefined or null to object
at Object.values (<anonymous>:null:null)
at LivePreview (src/app/_components/builder/LivePreview.jsx:105:21)
at BuilderLayout (src/app/_components/builder/BuilderLayout.jsx:68:9)
at FormBuilderPage (src/app/builder/[formId]/page.js:39:10)
## Code Frame
103 | <p className="text-gray-400 text-center py-8">Add fields to see preview</p>
104 | ) : (
> 105 | <List
| ^
106 | height={600} // Adjust to your preview container's height
107 | itemCount={deferredFields.length}
108 | itemSize={120} // Approximate height of each field row (in pixels)
Next.js version: 16.1.6 (Turbopack)
'use client';
import React from "react";
import { Input, TextArea, Checkbox, FieldLabel } from "@/components/ui";
import { List } from 'react-window';
import { useDeferredValue } from 'react';
// This is a separate component that renders ONE field (used by the List)
const FieldRow = ({ index, style, data }) => {
const field = data[index];
if (!field || !field.config) {
return <div style={style} />;
} ;
return (
<div style={style} key={field.id} className="space-y-2">
<FieldLabel
label={field.config.label}
required={field.config.required}
htmlFor={field.id}
/>
{/* Render appropriate input based on field type */}
{['EMAIL', 'PHONE'].includes(field.type) && (
<Input
type={field.type === 'EMAIL' ? 'email' : 'text'}
placeholder={field.config.placeholder}
disabled={true}
className="px-3 py-2 border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
)}
{field.type === 'NUMBER' && (
<Input
type="number"
placeholder={field.config.placeholder}
disabled
className="px-3 py-2 border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
)}
{field.type === 'SHORT_TEXT' && (
<Input
type="text"
placeholder={field.config.placeholder}
disabled
className="px-3 py-2 border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
)}
{field.type === 'TEXTAREA' && (
<TextArea
id={field.id}
value=""
onChange={() => {}}
placeholder={field.config.placeholder}
rows={3}
disabled
/>
)}
{field.type === 'DATE' && (
<Input
type="date"
disabled
className="border border-gray-300 focus:ring-2 focus:ring-blue-500"
/>
)}
{field.type === 'CHECKBOX' && Array.isArray(field.config.options) && (
<div className="space-y-2">
{field.config.options.map((option, idx) => (
<Checkbox
key={idx}
label={option}
disabled
checked={false}
/>
))}
</div>
)}
</div>
);
};
const LivePreview = ({ fields = []}) => {
const safeFields = (fields || []).filter(Boolean);
const deferredFields = useDeferredValue(safeFields);
return (
<aside className="w-96 bg-gray-100 border-l border-gray-200 p-6 overflow-y-auto">
<div className="mb-4">
<h2 className="text-lg font-bold text-gray-900 mb-2">Preview</h2>
<p className="text-sm text-gray-500">Live preview of your form</p>
</div>
<div className="bg-white rounded-lg shadow-sm p-6 border border-gray-200">
{deferredFields.length === 0 ? (
<p className="text-gray-400 text-center py-8">Add fields to see preview</p>
) : (
<List
height={600} // Adjust to your preview container's height
itemCount={deferredFields.length}
itemSize={120} // Approximate height of each field row (in pixels)
itemData={deferredFields}
width="100%"
>
{FieldRow}
</List>
)}
</div>
</aside>
);
};
export default React.memo(LivePreview);
'use client';
import { DndContext, DragOverlay, closestCenter } from '@dnd-kit/core';
import { useState, useCallback } from 'react';
import FieldsSidebar from './FieldsSidebar';
import FormCanvas from './FormCanvas';
import LivePreview from './LivePreview';
import { FIELD_TYPES } from '@/lib/builder/fieldRegistry';
export default function BuilderLayout({ builderState, formId }) {
const [activeId, setActiveId] = useState(null);
const handleDragStart = useCallback((event) => {
setActiveId(event.active.id);
}, []);
const handleDragEnd = useCallback((event) => {
const { active, over } = event;
if (!over) {
setActiveId(null);
return;
}
// Dragging from sidebar to canvas
if (active.id.startsWith('sidebar-')) {
const fieldType = active.id.replace('sidebar-', '');
builderState.addField(fieldType);
}
// Reordering within canvas
if (active.id.startsWith('field-') && over.id.startsWith('field-')) {
const oldIndex = builderState.fields.findIndex(
(f) => `field-${f.id}` === active.id
);
const newIndex = builderState.fields.findIndex(
(f) => `field-${f.id}` === over.id
);
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
builderState.reorderFields(oldIndex, newIndex);
}
}
setActiveId(null);
}, [builderState]);
console.log('BuilderLayout render', builderState.fields.length);
const activeField = activeId?.startsWith('sidebar-')
? FIELD_TYPES[activeId.replace('sidebar-', '')]
: null;
return (
<DndContext
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex h-screen bg-gray-50">
{/* Left Sidebar - Field Types */}
<FieldsSidebar />
{/* Center Canvas - Drop Zone */}
<FormCanvas builderState={builderState} />
{/* Right Preview - Live View */}
<LivePreview fields={builderState.fields} />
{/* Drag Overlay */}
<DragOverlay>
{activeField ? (
<div className="bg-white p-4 rounded-lg shadow-lg border-2 border-blue-500">
{activeField.icon} {activeField.label}
</div>
) : null}
</DragOverlay>
</div>
</DndContext>
);
}