Dynamic Report Generator with Live Preview Using React & Zustand

Building a Dynamic Report Generator with Live Preview Using React & Zustand

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

ts
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

ts
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:

tsx
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

css
.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:

tsx
<div className="images-grid">
{section.images.map((src, idx) => <img key={idx} src={src} alt="" />)}
{section.images.length < maxImages && <AddImageButton onAdd={...} />}
</div>
  • auto-fill and minmax ensure flexible, responsive layouts regardless of image count.


7. Editable Tables per Section

tsx
{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:

tsx
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

tsx
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 in localStorage.

  • 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!

Talk To Our Experts!

SHARE

Talk To Our Experts!

By filling the form, you agree to our Terms & Conditions and Privacy Policy.

100% privacy. We’ll contact you within 24 hrs. No spam.