Introduction
Modern web apps increasingly require dynamic document/report builders that let users assemble content—titles, paragraphs, images, tables—and preview changes instantly. Learn how to build a dynamic report generator with live preview using React, Zustand, and CSS Grid. Discover how to manage editable sections, responsive image layouts, and real-time updates efficiently in your web app. While tools like Gutenberg or Notion offer this out of the box, rolling your own solution provides ultimate control and freedom. In this guide, we dive into a practical implementation using React, Zustand, and CSS Grid to build a robust report generation module with live preview, editable content, dynamic sections, and responsive layouts.
1. Architectural Overview
State Management: Use Zustand to maintain a global store of sections. Each section contains a unique ID, type, optional table, images, and textual fields.
Live Preview: Inputs bind directly to state. UI components and preview panels share the same store, so edits reflect instantly.
Responsiveness: Use CSS Grid (or Flexbox) to adaptively render 1–n images per section.
Section Actions: Encapsulate copy/delete behaviors using store actions and section IDs.
Performance: Leverage
React.memo
,useCallback
, and Zustand’s selectors to avoid unnecessary re-renders.
2. Why Zustand?
While Context or Redux are popular, Zustand offers several advantages for dynamic UIs:
Component Decoupling: Zustand decouples state from components and avoids Context prop‑drilling Reddit.
Selective Subscriptions: Components can subscribe to exact slices of state, preventing full‑tree re-renders .
Simplicity and Scalability: Lightweight, no boilerplate, built-in support for immutability.
3. Core Data Model
type Section = {
id: string;
title: string;
description: string;
images: string[];
hasTable: boolean;
table: { rows: string[][] };
};type ReportState = {
sections: Section[];
addSection: (afterId?: string) => void;
updateSection: (id: string, patch: Partial<Section>) => void;
copySection: (id: string) => void;
deleteSection: (id: string) => void;
};
Each section is uniquely identified to safely clone or remove them without breaking the report structure.
4. Setting Up the Zustand Store
import create from 'zustand';
import { nanoid } from 'nanoid';export const useReportStore = create<ReportState>(set => ({
sections: [],
addSection: (afterId) => set(state => {
const newSection: Section = {
id: nanoid(),
title: '',
description: '',
images: [],
hasTable: false,
table: { rows: [['']] },
};
const sections = [...state.sections];
const idx = afterId ? sections.findIndex(s => s.id === afterId) + 1 : sections.length;
sections.splice(idx, 0, newSection);
return { sections };
}),
updateSection: (id, patch) => set(state => ({
sections: state.sections.map(s => s.id === id ? { ...s, ...patch } : s)
})),
copySection: (id) => set(state => {
const idx = state.sections.findIndex(s => s.id === id);
if (idx === -1) return {};
const copy = { ...state.sections[idx], id: nanoid() };
const sections = [...state.sections];
sections.splice(idx + 1, 0, copy);
return { sections };
}),
deleteSection: (id) => set(state => ({
sections: state.sections.filter(s => s.id !== id)
})),
}));
Unique IDs make copying and deletion reliable.
Patches keep updates atomic and sections isolated.
5. Building the Section Editor Component
Each section renders with inputs tied directly to store via selectors:
const SectionEditor = React.memo(({ id }) => {
const section = useReportStore(state => state.sections.find(s => s.id === id));
const update = useReportStore(state => state.updateSection);
const copy = useReportStore(state => state.copySection);
const remove = useReportStore(state => state.deleteSection); if (!section) return null;
return (
<div className="section-editor">
<input
type="text"
value={section.title}
onChange={e => update(id, { title: e.target.value })}
placeholder="Section title"
/>
<textarea
value={section.description}
onChange={e => update(id, { description: e.target.value })}
placeholder="Section description"
/>
{/* image upload + grid */}
{/* table editor toggle */}
<div className="actions">
<button onClick={() => copy(id)}>Copy</button>
<button onClick={() => remove(id)}>Delete</button>
</div>
</div>
);
});
React.memo
prevents rerenders when other sections change.Selectors isolate each section’s updates.
6. Responsive Image Layout via CSS Grid
.images-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.images-grid img {
width: 100%;
height: auto;
object-fit: cover;
}
In JSX:
<div className="images-grid">
{section.images.map((src, idx) => <img key={idx} src={src} alt="" />)}
{section.images.length < maxImages && <AddImageButton onAdd={...} />}
</div>
auto-fill
andminmax
ensure flexible, responsive layouts regardless of image count.
7. Editable Tables per Section
{section.hasTable && (
<TableEditor
table={section.table}
onChange={(table) => update(id, { table })}
/>
)}
<button
onClick={() =>
update(id, { hasTable: !section.hasTable })
}
>
{section.hasTable ? 'Remove Table' : 'Add Table'}
</button>
TableEditor
can manage row/col additions internally or via callbacks—its state originates and persists in the global store.
8. Live Preview Implementation
Create a ReportPreview
component that reads the full sections
array:
function ReportPreview() {
const sections = useReportStore(state => state.sections);
return (
<div className="report-preview">
{sections.map(s => (
<div key={s.id} className="preview-section">
<h2>{s.title || 'Untitled'}</h2>
<p>{s.description}</p>
<div className="images-grid">
{s.images.map((src, idx) => <img key={idx} src={src} alt="" />)}
</div>
{s.hasTable && <RenderedTable table={s.table} />}
</div>
))}
</div>
);
}
Every change in the editor updates Zustand, which in turn triggers the preview to refresh instantly.
9. Performance Optimization Techniques
Selective Selectors:
useReportStore(state => selector(state))
ensures components only rerender when relevant state changes.Memoized Callbacks: Wrap callbacks in
useCallback
to preserve referential equality.Memo Components: Editors and PreviewSection components are memoized to avoid unnecessary rerenders.
Batch Updates: Combine multiple updates into a single
set
call to prevent extra renders.
10. Handling Copy & Delete Robustly
Copy: Generates a new section with same content but fresh ID—preserves insert order because it’s injected immediately after the origin.
Delete: Filters state array using immutable remove—safe and controlled.
Undo/Redo Support: Zustand middleware (like
immer
,persist
,devtools
) can be layered for full-blown undo/redo.
11. Putting It All Together: App Shell
function ReportBuilder() {
const addSection = useReportStore(state => state.addSection);
const sections = useReportStore(state => state.sections); return (
<div className="report-builder">
{sections.map(s => <SectionEditor key={s.id} id={s.id} />)}
<button onClick={() => addSection()}>Add Section</button>
<hr />
<h1>Live Preview</h1>
<ReportPreview />
</div>
);
}
With minimal wiring, you get:
Full control over dynamic content sections
Live two-pane preview/editor interface
Editable multimedia and table support
Copy/delete and ordering features
Good performance via memoization and selective updates
12. Beyond the MVP: Advanced Features
Drag-and-drop section reordering using tools like
dnd-kit
.Persistent reports via Zustand’s
persist
middleware to store inlocalStorage
.Export to PDF or Word using HTML-to-PDF libraries (e.g.
jsPDF
,html2pdf
,docx
).Custom styling options per section — allow users to choose layouts, fonts, or colors.
Collaboration support — sync the Zustand store in real-time using WebSockets or Firebase.
13. Conclusion
Building a highly flexible, dynamic report editor with live preview is achievable thanks to:
Zustand for lightweight yet powerful global state management
React.memo and selectors for rendering optimization
CSS Grid for responsive multimedia layout
Clean section-based architecture to support copy/delete/edit features
This architecture scales from simple forms to sophisticated, multi-media report-building tools. By combining these techniques, developers can create engaging, user-friendly document builders—perfect for whitepapers, client deliverables, or content platforms.
We hope this guide empowers you to integrate dynamic report generation into your own apps. Happy building!