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

PropTypeDefaultDescription
documentIdstring | null-The unique identifier of the loaded document.
documentUrlstring | null-The URL of the PDF document being displayed.
isLoadingboolean-Whether the document is currently loading.
errorstring | null-Error message if document loading failed.
totalPagesnumber-Total number of pages in the document.
currentPagenumber-The current page being viewed (1-indexed).
pagesPageInfo[]-Array of page information with dimensions and rotation.
fieldsMap<string, FieldState>-Map of all fields in the document keyed by field ID.
selectedFieldIdstring | null-ID of the currently selected field.
renderContextRenderContext-Current rendering context with scale and viewport info.
isDirtyboolean-Whether the document has unsaved changes.

Actions

PropTypeDefaultDescription
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