Skip to content

Sponsor

Sponsor

form.toObject()

Quick Start (30 seconds)

javascript
const form = Forms.create({
  email: 'user@example.com',
  password: 'secret123',
  remember: true
});

// Convert reactive form to plain object
const plainObject = form.toObject();

console.log(plainObject);
// {
//   email: 'user@example.com',
//   password: 'secret123',
//   remember: true
// }

// Use with JSON.stringify
const json = JSON.stringify(form.toObject());

// Send to API
await fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify(form.toObject())
});

What just happened? toObject() converted the reactive form state into a plain JavaScript object, perfect for serialization and API calls!

What is form.toObject()?

form.toObject() is a serialization method that converts the reactive form into a plain JavaScript object.

Simply put, it extracts the raw values from the reactive form state and returns them as a normal object without reactivity or Proxy wrappers.

Key characteristics:

  • ✅ Returns plain object (no Proxy, no reactivity)
  • ✅ Contains only form values
  • ✅ Excludes errors, touched state, metadata
  • ✅ Safe for JSON.stringify()
  • ✅ Safe for cloning/spreading
  • ✅ Perfect for API submissions
  • ✅ Snapshot of current form state

Syntax

javascript
// Get plain object from form
const plainObject = form.toObject();

// Use with JSON
const json = JSON.stringify(form.toObject());

// Use with API
await fetch('/api/endpoint', {
  method: 'POST',
  body: JSON.stringify(form.toObject())
});

// Use with localStorage
localStorage.setItem('formData', JSON.stringify(form.toObject()));

// Clone form values
const snapshot = { ...form.toObject() };

Parameters: None

Returns: Object - Plain JavaScript object with form values

Return Structure:

javascript
{
  field1: value1,
  field2: value2,
  // ... all form fields
}

Why Does This Exist?

The Challenge: Reactive Objects Aren't Plain Objects

Reactive form values are wrapped in Proxies for reactivity. This causes issues when serializing.

javascript
const form = Forms.create({
  email: '',
  password: ''
});

// ❌ Using form.values directly with JSON.stringify
const json = JSON.stringify(form.values);
// Might include Proxy artifacts, internal metadata, or fail entirely

// ❌ Sending reactive object to API
await fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify(form.values)
  // Proxy can cause serialization issues
});

// ❌ Storing in localStorage
localStorage.setItem('form', JSON.stringify(form.values));
// May not serialize correctly

// ❌ Comparing form state
const before = form.values;
form.setValue('email', 'new@example.com');
const after = form.values;

console.log(before === after); // Still true! Same Proxy reference
// Can't detect changes by comparing references

Problems:Proxy artifacts - Reactive wrapper may interfere with serialization ❌ Extra properties - Might include internal tracking data ❌ JSON serialization - Proxies don't always serialize correctly ❌ API compatibility - Some APIs reject non-plain objects ❌ Comparison issues - Can't use reference equality ❌ Deep cloning - Difficult to snapshot form state

The Solution with toObject()

javascript
const form = Forms.create({
  email: '',
  password: ''
});

// ✅ Safe JSON serialization
const json = JSON.stringify(form.toObject());

// ✅ Safe API submission
await fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify(form.toObject())
});

// ✅ Safe localStorage
localStorage.setItem('form', JSON.stringify(form.toObject()));

// ✅ Proper snapshots
const before = form.toObject();
form.setValue('email', 'new@example.com');
const after = form.toObject();

console.log(before === after); // false - different objects
console.log(JSON.stringify(before) === JSON.stringify(after)); // false

Benefits:Guaranteed plain object - No Proxy wrappers ✅ Clean values only - No errors, touched state, or metadata ✅ Safe serialization - Works perfectly with JSON.stringify() ✅ API-ready - Can be sent directly to any API ✅ Proper cloning - Easy to snapshot form state ✅ Comparison-friendly - Can compare snapshots

Mental Model

Think of toObject() as a form snapshot camera - it captures the current values as a plain picture you can save, send, or compare.

Visual Flow

Reactive Form State
┌─────────────────────────┐
│ Proxy Wrapper           │
│ ┌─────────────────────┐ │
│ │ values: {          │ │
│ │   email: '...',    │ │
│ │   password: '...'  │ │
│ │ }                  │ │
│ │ errors: {...}      │ │
│ │ touched: {...}     │ │
│ │ isValid: true      │ │
│ │ [internal state]   │ │
│ └─────────────────────┘ │
└─────────────────────────┘

   form.toObject()

Plain JavaScript Object
┌─────────────────────────┐
│ {                       │
│   email: '...',         │
│   password: '...'       │
│ }                       │
│                         │
│ ✓ No Proxy              │
│ ✓ No metadata           │
│ ✓ Just values           │
└─────────────────────────┘

Real-World Analogy

Reactive Form (Living Person):

A person is constantly changing:
- Breathing, moving, thinking
- Has complex internal state
- Can't be easily copied or stored
- Reacts to environment

toObject() (Photograph):

Taking a photo of the person:
- Captures their appearance at one moment
- Static, unchanging snapshot
- Can be easily copied, sent, or stored
- No longer reactive, just data

How Does It Work?

Internal Process

javascript
// When you call:
const plainObject = form.toObject();

// Here's what happens internally:
function toObject() {
  1️⃣ Create empty plain object
     const result = {};

  2️⃣ Extract values from reactive form
     const values = form.values;

  3️⃣ Copy each field to plain object
     for (const key in values) {
       const value = values[key];

       4️⃣ Handle different value types
          if (isPrimitive(value)) {
            result[key] = value; // Copy directly
          } else if (Array.isArray(value)) {
            result[key] = [...value]; // Clone array
          } else if (isObject(value)) {
            result[key] = { ...value }; // Clone object
          }
     }

  5️⃣ Return plain object
     return result;
}

Conversion Flow Diagram

form.toObject()

Access form.values

For each field:

┌────────────────────────┐
│ Extract Value          │
│ - Remove Proxy wrapper │
│ - Clone if needed      │
│ - Preserve type        │
└────────────────────────┘

Build plain object

┌────────────────────────┐
│ Plain Object           │
│ {                      │
│   field1: value1,      │
│   field2: value2       │
│ }                      │
└────────────────────────┘

Return to caller

Basic Usage

Example 1: API Submission

javascript
const form = Forms.create({
  username: 'john_doe',
  email: 'john@example.com',
  password: 'secret123'
});

// Submit to API
async function register() {
  const response = await fetch('/api/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(form.toObject())
  });

  return await response.json();
}

// Output sent to API:
// {
//   "username": "john_doe",
//   "email": "john@example.com",
//   "password": "secret123"
// }

Example 2: LocalStorage Persistence

javascript
const form = Forms.create({
  firstName: '',
  lastName: '',
  email: ''
});

// Save form state to localStorage
function saveFormState() {
  const formData = form.toObject();
  localStorage.setItem('userForm', JSON.stringify(formData));
}

// Load form state from localStorage
function loadFormState() {
  const saved = localStorage.getItem('userForm');
  if (saved) {
    const formData = JSON.parse(saved);
    form.setValues(formData);
  }
}

// Auto-save on change
effect(() => {
  // Trigger on any value change
  JSON.stringify(form.values);
  saveFormState();
});

Example 3: Form State Comparison

javascript
const form = Forms.create({
  title: '',
  content: ''
});

// Take initial snapshot
const initialState = form.toObject();

// User makes changes
form.setValue('title', 'My Article');
form.setValue('content', 'Article content...');

// Check if form has changed
const currentState = form.toObject();
const hasChanged = JSON.stringify(initialState) !== JSON.stringify(currentState);

if (hasChanged) {
  console.log('Form has unsaved changes');
  showUnsavedWarning();
}

Example 4: Debugging and Logging

javascript
const form = Forms.create({
  email: '',
  password: ''
});

// Log form state
function debugForm() {
  console.log('Current form values:', form.toObject());
  console.log('Form errors:', form.errors);
  console.log('Touched fields:', form.touchedFields);
}

// Log on every change
effect(() => {
  JSON.stringify(form.values);
  console.log('Form updated:', form.toObject());
});

Example 5: Cloning Form State

javascript
const originalForm = Forms.create({
  name: 'John Doe',
  email: 'john@example.com'
});

// Create a new form with same values
const snapshot = originalForm.toObject();
const clonedForm = Forms.create(snapshot);

// Modify clone without affecting original
clonedForm.setValue('name', 'Jane Doe');

console.log('Original:', originalForm.toObject());
// { name: 'John Doe', email: 'john@example.com' }

console.log('Clone:', clonedForm.toObject());
// { name: 'Jane Doe', email: 'john@example.com' }

Advanced Patterns

Pattern 1: Form Versioning and Undo

javascript
const form = Forms.create({
  title: '',
  content: ''
});

const history = [];
let historyIndex = -1;

// Save state to history
function saveToHistory() {
  const snapshot = form.toObject();

  // Remove future history if we're not at the end
  history.splice(historyIndex + 1);

  // Add new snapshot
  history.push(snapshot);
  historyIndex = history.length - 1;

  // Limit history size
  if (history.length > 50) {
    history.shift();
    historyIndex--;
  }
}

// Undo
function undo() {
  if (historyIndex > 0) {
    historyIndex--;
    form.setValues(history[historyIndex]);
  }
}

// Redo
function redo() {
  if (historyIndex < history.length - 1) {
    historyIndex++;
    form.setValues(history[historyIndex]);
  }
}

// Save on every change
let saveTimeout;
effect(() => {
  JSON.stringify(form.values);

  clearTimeout(saveTimeout);
  saveTimeout = setTimeout(saveToHistory, 500);
});

Pattern 2: Multi-Tab Sync

javascript
const form = Forms.create({
  email: '',
  message: ''
});

// Sync form across browser tabs
function syncFormAcrossTabs() {
  // Save to localStorage on change
  effect(() => {
    const data = form.toObject();
    localStorage.setItem('sharedForm', JSON.stringify(data));
  });

  // Listen for changes from other tabs
  window.addEventListener('storage', (e) => {
    if (e.key === 'sharedForm' && e.newValue) {
      const newData = JSON.parse(e.newValue);

      // Update form if data changed
      const currentData = form.toObject();
      if (JSON.stringify(currentData) !== JSON.stringify(newData)) {
        form.setValues(newData);
      }
    }
  });
}

syncFormAcrossTabs();

Pattern 3: Dirty Checking and Confirmation

javascript
const form = Forms.create({
  title: '',
  content: ''
});

const originalData = form.toObject();

// Check if form is dirty
function isDirty() {
  const currentData = form.toObject();
  return JSON.stringify(originalData) !== JSON.stringify(currentData);
}

// Warn before leaving page
window.addEventListener('beforeunload', (e) => {
  if (isDirty()) {
    e.preventDefault();
    e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
    return e.returnValue;
  }
});

// Warn before navigation
function navigateAway(url) {
  if (isDirty()) {
    const confirmed = confirm('You have unsaved changes. Continue?');
    if (!confirmed) return;
  }

  window.location.href = url;
}

Pattern 4: Form Diff Detection

javascript
const form = Forms.create({
  username: 'john',
  email: 'john@example.com',
  bio: 'Software developer'
});

const originalState = form.toObject();

// Get changed fields
function getChangedFields() {
  const currentState = form.toObject();
  const changes = {};

  for (const key in currentState) {
    if (currentState[key] !== originalState[key]) {
      changes[key] = {
        old: originalState[key],
        new: currentState[key]
      };
    }
  }

  return changes;
}

// Save only changed fields
async function saveChanges() {
  const changes = getChangedFields();

  if (Object.keys(changes).length === 0) {
    console.log('No changes to save');
    return;
  }

  // Send only changed fields to API
  const payload = {};
  for (const key in changes) {
    payload[key] = changes[key].new;
  }

  await fetch('/api/user/update', {
    method: 'PATCH',
    body: JSON.stringify(payload)
  });

  console.log('Saved changes:', changes);
}

Pattern 5: Form Export/Import

javascript
const form = Forms.create({
  name: '',
  email: '',
  preferences: {}
});

// Export form to JSON file
function exportForm() {
  const data = form.toObject();
  const json = JSON.stringify(data, null, 2);
  const blob = new Blob([json], { type: 'application/json' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = 'form-data.json';
  a.click();

  URL.revokeObjectURL(url);
}

// Import form from JSON file
function importForm(file) {
  const reader = new FileReader();

  reader.onload = (e) => {
    try {
      const data = JSON.parse(e.target.result);
      form.setValues(data);
      console.log('Form imported successfully');
    } catch (error) {
      console.error('Invalid JSON file:', error);
    }
  };

  reader.readAsText(file);
}

// Usage
exportButton.addEventListener('click', exportForm);
importInput.addEventListener('change', (e) => importForm(e.target.files[0]));

Pattern 6: Form Snapshot Comparison

javascript
const form = Forms.create({
  email: '',
  username: ''
});

const snapshots = [];

// Take periodic snapshots
setInterval(() => {
  snapshots.push({
    timestamp: Date.now(),
    data: form.toObject()
  });

  // Keep only last 10 snapshots
  if (snapshots.length > 10) {
    snapshots.shift();
  }
}, 5000);

// Compare with previous snapshot
function compareWithPrevious() {
  if (snapshots.length < 2) return null;

  const current = snapshots[snapshots.length - 1].data;
  const previous = snapshots[snapshots.length - 2].data;

  const differences = {};
  for (const key in current) {
    if (current[key] !== previous[key]) {
      differences[key] = {
        from: previous[key],
        to: current[key]
      };
    }
  }

  return differences;
}

Pattern 7: Batch API Submission

javascript
const forms = [
  Forms.create({ name: 'Form 1', data: '' }),
  Forms.create({ name: 'Form 2', data: '' }),
  Forms.create({ name: 'Form 3', data: '' })
];

// Submit all forms as batch
async function submitBatch() {
  const batch = forms.map(form => form.toObject());

  const response = await fetch('/api/batch-submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(batch)
  });

  return await response.json();
}

// Payload sent to API:
// [
//   { name: 'Form 1', data: '...' },
//   { name: 'Form 2', data: '...' },
//   { name: 'Form 3', data: '...' }
// ]

Pattern 8: Form State Caching

javascript
const form = Forms.create({
  searchQuery: '',
  filters: {}
});

const cache = new Map();

// Cache form state with key
function cacheFormState(key) {
  const data = form.toObject();
  cache.set(key, {
    data,
    timestamp: Date.now()
  });
}

// Restore from cache
function restoreFromCache(key) {
  const cached = cache.get(key);

  if (cached) {
    const age = Date.now() - cached.timestamp;

    // Use cache if less than 5 minutes old
    if (age < 5 * 60 * 1000) {
      form.setValues(cached.data);
      return true;
    }
  }

  return false;
}

// Auto-cache on search
searchButton.addEventListener('click', () => {
  const query = form.values.searchQuery;
  cacheFormState(`search:${query}`);
});

Pattern 9: Form Validation Before Serialization

javascript
const form = Forms.create(
  {
    email: '',
    password: ''
  },
  {
    email: (value) => !value.includes('@') ? 'Invalid email' : '',
    password: (value) => value.length < 8 ? 'Too short' : ''
  }
);

// Safe serialization with validation
function toValidObject() {
  form.validate();

  if (!form.isValid) {
    throw new Error('Form has validation errors');
  }

  return form.toObject();
}

// Use it
try {
  const data = toValidObject();

  await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify(data)
  });
} catch (error) {
  console.error('Cannot serialize invalid form:', error);
  showErrors();
}

Pattern 10: Form State Encryption

javascript
const form = Forms.create({
  ssn: '',
  creditCard: ''
});

// Encrypt sensitive data before serializing
async function toEncryptedObject(encryptionKey) {
  const data = form.toObject();
  const encrypted = {};

  for (const [key, value] of Object.entries(data)) {
    if (isSensitiveField(key)) {
      encrypted[key] = await encrypt(value, encryptionKey);
    } else {
      encrypted[key] = value;
    }
  }

  return encrypted;
}

function isSensitiveField(field) {
  return ['ssn', 'creditCard', 'password'].includes(field);
}

async function encrypt(value, key) {
  // Use Web Crypto API
  const encoder = new TextEncoder();
  const data = encoder.encode(value);

  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: new Uint8Array(12) },
    key,
    data
  );

  return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}

// Usage
const encryptionKey = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 },
  true,
  ['encrypt', 'decrypt']
);

const encrypted = await toEncryptedObject(encryptionKey);
await saveSecurely(encrypted);

Common Pitfalls

Pitfall 1: Modifying Returned Object

javascript
const form = Forms.create({ email: '' });

// ❌ Modifying returned object doesn't update form
const obj = form.toObject();
obj.email = 'new@example.com';

console.log(form.values.email); // Still '' - form not updated!

// ✅ Use setValue to update form
form.setValue('email', 'new@example.com');

Pitfall 2: Assuming Reactivity Persists

javascript
const form = Forms.create({ count: 0 });

// ❌ Plain object is not reactive
const obj = form.toObject();

effect(() => {
  console.log(obj.count); // Won't react to changes
});

form.setValue('count', 1); // Effect doesn't run

// ✅ Use form.values for reactive access
effect(() => {
  console.log(form.values.count); // Reactive
});

Pitfall 3: Not Deep Cloning Nested Objects

javascript
const form = Forms.create({
  user: { name: 'John', age: 30 }
});

// ❌ Shallow copy - nested objects may still be reactive
const obj = form.toObject();
obj.user.name = 'Jane'; // Might mutate form!

// ✅ Deep clone if needed
const obj = JSON.parse(JSON.stringify(form.toObject()));

Pitfall 4: Including Sensitive Data

javascript
const form = Forms.create({
  username: 'john',
  password: 'secret123',
  passwordConfirm: 'secret123'
});

// ❌ Sending password in plain object
const data = form.toObject();
console.log(data); // Logs password!

await analytics.track('form_submitted', data); // Logs password to analytics!

// ✅ Filter sensitive fields
function toSafeObject() {
  const data = form.toObject();
  const { password, passwordConfirm, ...safe } = data;
  return safe;
}

await analytics.track('form_submitted', toSafeObject());

Pitfall 5: Forgetting to Validate

javascript
const form = Forms.create(
  { email: '' },
  { email: (value) => !value ? 'Required' : '' }
);

// ❌ Serializing without validation
const data = form.toObject();
await submitToAPI(data); // Might send invalid data!

// ✅ Validate before serializing
form.validate();
if (form.isValid) {
  const data = form.toObject();
  await submitToAPI(data);
}

Summary

Key Takeaways

  1. toObject() returns plain JavaScript object - no Proxy, no reactivity.

  2. Safe for serialization - works perfectly with JSON.stringify().

  3. Values only - excludes errors, touched state, and metadata.

  4. Snapshot in time - captures current form state.

  5. API-ready - can be sent directly to any API.

  6. Perfect for persistence - localStorage, sessionStorage, IndexedDB.

When to Use toObject()

Use toObject() for:

  • Sending data to APIs
  • JSON serialization
  • LocalStorage/SessionStorage persistence
  • Form state comparison
  • Creating snapshots
  • Debugging/logging
  • Cloning form state

Don't use toObject() when:

  • Need reactive access to values
  • Want to include errors or touched state
  • Need to update form (use setValue instead)

Comparison: form.values vs form.toObject()

Aspectform.valuesform.toObject()
TypeReactive ProxyPlain object
Reactivity✅ Reactive❌ Not reactive
Serialization⚠️ May have issues✅ Safe
API submission⚠️ Use with caution✅ Perfect
Snapshot❌ Same reference✅ New object
JSON.stringify⚠️ May fail✅ Works
MutationsUpdate formDon't update form

Returned Object Structure

javascript
const form = Forms.create({
  email: 'user@example.com',
  password: 'secret',
  remember: true
});

form.toObject();
// Returns:
{
  email: 'user@example.com',
  password: 'secret',
  remember: true
}

// Does NOT include:
// - errors
// - touched
// - isValid
// - isDirty
// - isSubmitting
// - submitCount

Typical Usage Pattern

javascript
// 1. Create form
const form = Forms.create({ email: '', password: '' });

// 2. User fills form
form.setValue('email', 'user@example.com');
form.setValue('password', 'secret123');

// 3. Validate
form.validate();

// 4. Convert to plain object
if (form.isValid) {
  const data = form.toObject();

  // 5. Send to API
  await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify(data)
  });
}
  • setValues(obj) - Set multiple values from plain object
  • reset(values?) - Reset form to initial or new values
  • getValue(field) - Get single field value

One-Line Rule

form.toObject() converts the reactive form state into a plain JavaScript object containing only the form values, perfect for JSON serialization, API submission, and creating snapshots.

What's Next?

  • Explore advanced serialization patterns
  • Master form state persistence
  • Learn data transformation techniques