form.toObject()
Quick Start (30 seconds)
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
// 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:
{
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.
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 referencesProblems: ❌ 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()
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)); // falseBenefits: ✅ 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 environmenttoObject() (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 dataHow Does It Work?
Internal Process
// 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 callerBasic Usage
Example 1: API Submission
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
toObject()returns plain JavaScript object - no Proxy, no reactivity.Safe for serialization - works perfectly with JSON.stringify().
Values only - excludes errors, touched state, and metadata.
Snapshot in time - captures current form state.
API-ready - can be sent directly to any API.
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()
| Aspect | form.values | form.toObject() |
|---|---|---|
| Type | Reactive Proxy | Plain 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 |
| Mutations | Update form | Don't update form |
Returned Object Structure
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
// - submitCountTypical Usage Pattern
// 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)
});
}Related Methods
setValues(obj)- Set multiple values from plain objectreset(values?)- Reset form to initial or new valuesgetValue(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