Document Store
The useDocumentStore hook provides full access to the Zustand store that manages all document state and actions.
Import
import { useDocumentStore } from '@doccentral/react';Basic Usage
Full Store Accesstsx
function DocumentManager() {
// Get entire store (not recommended - causes re-renders on any change)
const store = useDocumentStore();
// Better: Select specific values
const currentPage = useDocumentStore((state) => state.currentPage);
const goToPage = useDocumentStore((state) => state.goToPage);
const totalPages = useDocumentStore((state) => state.totalPages);
return (
<div>
<button onClick={() => goToPage(currentPage - 1)} disabled={currentPage <= 1}>
Previous
</button>
<span>{currentPage} / {totalPages}</span>
<button onClick={() => goToPage(currentPage + 1)} disabled={currentPage >= totalPages}>
Next
</button>
</div>
);
}Selector Pattern
Always use selectors to subscribe to specific state slices. This prevents unnecessary re-renders and improves performance.
State Properties
| Prop | Type | Default | Description |
|---|---|---|---|
documentId | string | null | - | The unique identifier of the loaded document. |
documentUrl | string | null | - | The URL of the PDF document being displayed. |
isLoading | boolean | - | Whether the document is currently loading. |
error | string | null | - | Error message if document loading failed. |
totalPages | number | - | Total number of pages in the document. |
currentPage | number | - | The current page being viewed (1-indexed). |
pages | PageInfo[] | - | Array of page information with dimensions and rotation. |
fields | Map<string, FieldState> | - | Map of all fields in the document keyed by field ID. |
selectedFieldId | string | null | - | ID of the currently selected field. |
renderContext | RenderContext | - | Current rendering context with scale and viewport info. |
isDirty | boolean | - | Whether the document has unsaved changes. |
Actions
| Prop | Type | Default | Description |
|---|---|---|---|
initDocument | (documentId: string, documentUrl: string) => void | - | Initialize the store with a new document. |
setLoading | (isLoading: boolean) => void | - | Set the loading state. |
setError | (error: string | null) => void | - | Set an error message and clear loading state. |
setPages | (pages: PageInfo[]) => void | - | Set page information after PDF is loaded. |
goToPage | (page: number) => void | - | Navigate to a specific page (1-indexed). |
addField | (field: AnyFieldDefinition) => void | - | Add a new field to the document. |
updateFieldDefinition | (fieldId: string, updates: Partial<AnyFieldDefinition>) => void | - | Update field definition (position, size, etc.). |
setFieldValue | (fieldId: string, value: AnyFieldValue) => void | - | Set the value of a field. |
removeField | (fieldId: string) => void | - | Remove a field from the document. |
selectField | (fieldId: string | null) => void | - | Select a field or clear selection. |
setRenderContext | (context: Partial<RenderContext>) => void | - | Update the render context. |
touchField | (fieldId: string) => void | - | Mark a field as touched by the user. |
validateFields | () => boolean | - | Validate all fields and return whether all pass. |
reset | () => void | - | Reset the store to initial state. |
getField | (fieldId: string) => FieldState | undefined | - | Get a specific field by ID. |
getFieldsForPage | (page: number) => FieldState[] | - | Get all fields for a specific page. |
getSubmissionPayload | () => DocumentSubmissionPayload | null | - | Generate the submission payload for the API. |
Common Patterns
Page Navigation
Page Navigationtsx
function PageNavigation() {
const currentPage = useDocumentStore((s) => s.currentPage);
const totalPages = useDocumentStore((s) => s.totalPages);
const goToPage = useDocumentStore((s) => s.goToPage);
return (
<nav className="page-nav">
<button
onClick={() => goToPage(1)}
disabled={currentPage === 1}
>
First
</button>
<button
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage <= 1}
>
Previous
</button>
<input
type="number"
min={1}
max={totalPages}
value={currentPage}
onChange={(e) => goToPage(parseInt(e.target.value, 10))}
/>
<span>of {totalPages}</span>
<button
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage >= totalPages}
>
Next
</button>
<button
onClick={() => goToPage(totalPages)}
disabled={currentPage === totalPages}
>
Last
</button>
</nav>
);
}Field Management
Field Managementtsx
function FieldEditor({ fieldId }: { fieldId: string }) {
const field = useDocumentStore((s) => s.fields.get(fieldId));
const setFieldValue = useDocumentStore((s) => s.setFieldValue);
const removeField = useDocumentStore((s) => s.removeField);
const selectField = useDocumentStore((s) => s.selectField);
if (!field) return null;
const handleTextChange = (content: string) => {
setFieldValue(fieldId, { type: 'text', content });
};
const handleDelete = () => {
if (confirm('Delete this field?')) {
removeField(fieldId);
}
};
return (
<div
className="field-editor"
onClick={() => selectField(fieldId)}
>
{field.definition.type === 'text' && (
<input
type="text"
value={(field.value as TextFieldValue)?.content || ''}
onChange={(e) => handleTextChange(e.target.value)}
placeholder={field.definition.placeholder}
/>
)}
<button onClick={handleDelete}>Delete</button>
{field.errors.length > 0 && (
<div className="errors">
{field.errors.map((err, i) => <p key={i}>{err}</p>)}
</div>
)}
</div>
);
}Adding New Fields
Adding Fieldstsx
import { nanoid } from 'nanoid';
function AddFieldButton() {
const currentPage = useDocumentStore((s) => s.currentPage);
const addField = useDocumentStore((s) => s.addField);
const handleAddTextField = () => {
addField({
id: nanoid(),
type: 'text',
page: currentPage,
x: 0.1, // 10% from left
y: 0.1, // 10% from top
width: 0.3, // 30% of page width
height: 0.05, // 5% of page height
required: false,
editable: true,
label: 'New Text Field',
placeholder: 'Enter text...',
fontSize: 12,
fontFamily: 'Helvetica',
textAlign: 'left',
});
};
const handleAddSignatureField = () => {
addField({
id: nanoid(),
type: 'signature',
page: currentPage,
x: 0.1,
y: 0.8,
width: 0.3,
height: 0.1,
required: true,
editable: true,
label: 'Signature',
signatureMode: 'draw',
});
};
return (
<div className="add-field-buttons">
<button onClick={handleAddTextField}>Add Text Field</button>
<button onClick={handleAddSignatureField}>Add Signature</button>
</div>
);
}Validation and Submission
Validation & Submissiontsx
function SubmitButton() {
const { apiRequest } = useSDK();
const validateFields = useDocumentStore((s) => s.validateFields);
const getSubmissionPayload = useDocumentStore((s) => s.getSubmissionPayload);
const isDirty = useDocumentStore((s) => s.isDirty);
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async () => {
// Validate all fields
const isValid = validateFields();
if (!isValid) {
alert('Please fill in all required fields');
return;
}
// Get submission payload
const payload = getSubmissionPayload();
if (!payload) {
alert('No document loaded');
return;
}
setSubmitting(true);
try {
const result = await apiRequest('/sdk/documents/submit', {
method: 'POST',
body: JSON.stringify(payload),
});
console.log('Submitted:', result);
alert('Document submitted successfully!');
} catch (error) {
console.error('Submission failed:', error);
alert('Failed to submit document');
} finally {
setSubmitting(false);
}
};
return (
<button
onClick={handleSubmit}
disabled={!isDirty || submitting}
>
{submitting ? 'Submitting...' : 'Submit Document'}
</button>
);
}Render Context
The render context stores information about the current viewport and zoom level:
Render Contexttsx
function ZoomControls() {
const renderContext = useDocumentStore((s) => s.renderContext);
const setRenderContext = useDocumentStore((s) => s.setRenderContext);
const handleZoom = (delta: number) => {
const newScale = Math.max(0.5, Math.min(3, renderContext.scale + delta));
setRenderContext({ scale: newScale });
};
return (
<div className="zoom-controls">
<button onClick={() => handleZoom(-0.25)}>-</button>
<span>{Math.round(renderContext.scale * 100)}%</span>
<button onClick={() => handleZoom(0.25)}>+</button>
<button onClick={() => setRenderContext({ scale: 1 })}>Reset</button>
</div>
);
}
// RenderContext interface
interface RenderContext {
scale: number; // Zoom level (1 = 100%)
viewportWidth: number; // Viewport width in pixels
viewportHeight: number; // Viewport height in pixels
devicePixelRatio: number; // Device pixel ratio for HiDPI displays
}TypeScript Types
Store Typestypescript
interface DocumentState {
documentId: string | null;
documentUrl: string | null;
isLoading: boolean;
error: string | null;
totalPages: number;
currentPage: number;
pages: PageInfo[];
fields: Map<string, FieldState>;
selectedFieldId: string | null;
renderContext: RenderContext;
isDirty: boolean;
}
interface DocumentActions {
initDocument: (documentId: string, documentUrl: string) => void;
setLoading: (isLoading: boolean) => void;
setError: (error: string | null) => void;
setPages: (pages: PageInfo[]) => void;
goToPage: (page: number) => void;
addField: (field: AnyFieldDefinition) => void;
updateFieldDefinition: (fieldId: string, updates: Partial<AnyFieldDefinition>) => void;
setFieldValue: (fieldId: string, value: AnyFieldValue) => void;
removeField: (fieldId: string) => void;
selectField: (fieldId: string | null) => void;
setRenderContext: (context: Partial<RenderContext>) => void;
touchField: (fieldId: string) => void;
validateFields: () => boolean;
reset: () => void;
getField: (fieldId: string) => FieldState | undefined;
getFieldsForPage: (page: number) => FieldState[];
getSubmissionPayload: () => DocumentSubmissionPayload | null;
}
// The store is typed as DocumentState & DocumentActions
type DocumentStore = DocumentState & DocumentActions;Related
- useDocument - Read-only document state hook
- useSDK - SDK context and API access
- Types Reference - Complete TypeScript types