Skip to content

Sponsor

Sponsor

Understanding toRaw() - A Beginner's Guide

Quick Start (30 seconds)

Need to get the plain, non-reactive version of a reactive object? Here's how:

js
// Create reactive object
const reactiveUser = state({
  name: 'John',
  age: 25,
  email: 'john@example.com'
});

// Get the raw, non-reactive version
const plainUser = toRaw(reactiveUser);

// plainUser is a regular object
console.log(isReactive(reactiveUser));  // true
console.log(isReactive(plainUser));     // false

// Changes to plainUser won't trigger effects
plainUser.name = 'Jane';  // No effects triggered

That's it! The toRaw() function extracts the plain object from a reactive proxy!

What is toRaw()?

toRaw() is an extraction utility function that retrieves the original, non-reactive object from a reactive proxy. It "unwraps" reactive proxies to give you back the plain JavaScript object.

Getting raw values:

  • Extracts the original object from reactive proxies
  • Returns non-reactive values unchanged
  • Useful for serialization, comparisons, and external APIs
  • Breaks reactivity connection (changes won't trigger effects)

Think of it as removing a wrapper - you get the original item without the reactive packaging.

Syntax

js
// Using the shortcut
toRaw(value)

// Using the full namespace
ReactiveUtils.toRaw(value)

Both styles are valid! Choose whichever you prefer:

  • Shortcut style (toRaw()) - Clean and concise
  • Namespace style (ReactiveUtils.toRaw()) - Explicit and clear

Parameters:

  • value - Any value (reactive or non-reactive) (required)

Returns:

  • The original plain object if value is reactive
  • The value itself if not reactive

Why Does This Exist?

The Problem with Reactive Proxies in Certain Contexts

Let's say you need to work with external APIs or serialization:

javascript
const user = state({
  name: 'John',
  age: 25,
  email: 'john@example.com'
});

// Try to save to API
await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(user)  // Sends proxy, not plain object!
});

// Try to compare objects
const user2 = state({
  name: 'John',
  age: 25,
  email: 'john@example.com'
});

console.log(user === user2);  // false (different proxies!)

// Try to use with external library
const formatted = moment(user.createdAt);  // Might fail with proxy

This creates problems because reactive proxies aren't always compatible with:

  • JSON serialization (may include proxy metadata)
  • Deep equality comparisons
  • External libraries expecting plain objects
  • LocalStorage/SessionStorage
  • Console logging (proxies show wrapper, not data)

What's the Real Issue?

With Reactive Proxy:
┌──────────────────┐
│  Reactive Proxy  │
│  ┌────────────┐  │
│  │  { data }  │  │ ← Original object
│  └────────────┘  │
│                  │
│  + Reactivity    │
│  + Tracking      │
│  + Proxy Logic   │
└────────┬─────────┘


  JSON.stringify()
  localStorage.set()
  deepEqual()


  ❌ May not work
     as expected!

Problems: ❌ External APIs may not handle proxies correctly ❌ JSON serialization may include proxy metadata ❌ Deep comparisons don't work (different proxy instances) ❌ LocalStorage can't store proxies directly ❌ Console.log shows proxy wrapper, not clean data ❌ Third-party libraries expect plain objects

Why This Becomes a Problem:

When you need to:

  • Send data to APIs
  • Store data in localStorage
  • Compare objects for equality
  • Use with external libraries
  • Log clean data for debugging
  • Serialize for transmission

The Solution with toRaw()

When you use toRaw(), you get the original plain object:

javascript
const user = state({
  name: 'John',
  age: 25,
  email: 'john@example.com'
});

// Get raw object for API
const plainUser = toRaw(user);

await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(plainUser)  // Clean JSON!
});

// Compare raw objects
const user2 = state({
  name: 'John',
  age: 25,
  email: 'john@example.com'
});

const raw1 = toRaw(user);
const raw2 = toRaw(user2);

console.log(JSON.stringify(raw1) === JSON.stringify(raw2));  // true

// Use with external libraries
const raw = toRaw(user);
localStorage.setItem('user', JSON.stringify(raw));  // Works!

What Just Happened?

With toRaw():
┌──────────────────┐
│  Reactive Proxy  │
│  ┌────────────┐  │
│  │  { data }  │  │ ← Original object
│  └────────────┘  │
└────────┬─────────┘


    toRaw(proxy)


┌──────────────────┐
│   { data }       │ ← Plain object
│                  │
│   No proxy       │
│   No reactivity  │
└────────┬─────────┘


  JSON.stringify()
  localStorage.set()
  deepEqual()


  ✅ Works perfectly!

With toRaw():

  • Get clean, plain JavaScript objects
  • Works with all external APIs and libraries
  • Clean JSON serialization
  • Proper deep equality comparisons
  • LocalStorage compatible
  • Clean console output

Benefits: ✅ Extract plain objects from reactive proxies ✅ Works with external APIs and libraries ✅ Clean JSON serialization ✅ Enable deep equality comparisons ✅ Compatible with localStorage ✅ Clean debugging output

Mental Model

Think of toRaw() like unwrapping a gift:

Reactive Proxy (Gift-Wrapped):
┌─────────────────────────┐
│  🎁 Gift Wrap (Proxy)   │
│  ┌───────────────────┐  │
│  │                   │  │
│  │  🎁 Present       │  │
│  │  (Original Data)  │  │
│  │                   │  │
│  └───────────────────┘  │
│                         │
│  + Bow (Reactivity)     │
│  + Ribbon (Tracking)    │
└────────────┬────────────┘


        toRaw(gift)


┌─────────────────────────┐
│   🎁 Present            │
│   (Original Data)       │
│                         │
│   No wrapping           │
│   Just the gift         │
└─────────────────────────┘

Key Insight: Just like unwrapping a gift removes the decorative packaging to reveal what's inside, toRaw() removes the reactive proxy wrapper to reveal the plain object underneath.

How Does It Work?

The Magic: Symbol-Based Extraction

When you call toRaw(), here's what happens behind the scenes:

javascript
// What you write:
const plain = toRaw(reactiveObj);

// What actually happens (simplified):
// Every reactive proxy stores the original object
const RAW = Symbol('raw');

function toRaw(value) {
  // Check if value has the raw symbol
  return (value && value[RAW]) || value;
}

// When creating reactive proxies:
const proxy = new Proxy(target, {
  get(obj, key) {
    if (key === RAW) return target;  // Return original
    // ... rest of proxy logic
  }
});

In other words: toRaw():

  1. Checks if value exists
  2. Checks if value has the RAW symbol
  3. Returns the original object if found
  4. Returns the value itself if not reactive

Under the Hood

toRaw() implementation:
toRaw(value) {
  return (value && value[RAW]) || value;
}

Breaking it down:
- value          → Check if value exists
- value[RAW]     → Get original object from proxy
- || value       → Return value itself if not reactive

What happens:

1️⃣ Checks if value is truthy 2️⃣ Accesses the RAW symbol to get original 3️⃣ Returns original if found 4️⃣ Returns value itself if not reactive

Basic Usage

Extracting Plain Objects

The simplest way to use toRaw():

js
// Create reactive object
const reactive = state({
  name: 'John',
  age: 25
});

// Get plain object
const plain = toRaw(reactive);

console.log(isReactive(reactive));  // true
console.log(isReactive(plain));     // false

// Changes to plain won't trigger effects
plain.name = 'Jane';  // No effects!

Using with Non-Reactive Values

toRaw() is safe to use with any value:

js
const plainObj = { count: 0 };
const num = 42;
const str = 'hello';

console.log(toRaw(plainObj));  // { count: 0 }
console.log(toRaw(num));       // 42
console.log(toRaw(str));       // 'hello'

// Returns the value itself if not reactive

Serialization

Perfect for JSON operations:

js
const user = state({
  name: 'John',
  settings: {
    theme: 'dark',
    notifications: true
  }
});

// Serialize cleanly
const json = JSON.stringify(toRaw(user));
console.log(json);
// {"name":"John","settings":{"theme":"dark","notifications":true}}

// Save to localStorage
localStorage.setItem('user', JSON.stringify(toRaw(user)));

When to Use toRaw()

✅ Good Use Cases

1. API Requests

js
async function saveUser(user) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(toRaw(user))  // Clean JSON
  });
  return response.json();
}

2. LocalStorage/SessionStorage

js
function saveToStorage(key, state) {
  const raw = toRaw(state);
  localStorage.setItem(key, JSON.stringify(raw));
}

function loadFromStorage(key) {
  const json = localStorage.getItem(key);
  return json ? JSON.parse(json) : null;
}

3. Deep Equality Comparisons

js
function deepEqual(a, b) {
  const rawA = toRaw(a);
  const rawB = toRaw(b);
  return JSON.stringify(rawA) === JSON.stringify(rawB);
}

4. External Library Integration

js
function processWithLibrary(data) {
  // External libraries may not handle proxies
  const raw = toRaw(data);
  return externalLibrary.process(raw);
}

5. Debug Logging

js
function logState(state, label) {
  console.log(label, toRaw(state));
  // Clean output without proxy wrapper
}

❌ Not Needed

1. Normal State Access

js
// Don't use toRaw for normal operations
const raw = toRaw(user);
   console.log(raw.name);

// Just access directly
✅ console.log(user.name);

2. Within Effects

js
// Don't use toRaw in effects unnecessarily
effect(() => {
     const raw = toRaw(app);
     console.log(raw.count);
   });

// Just use the reactive value
effect(() => {
     console.log(app.count);
   });

Real-World Examples

Example 1: Persistent State Manager

js
class StateManager {
  constructor(initialState, storageKey) {
    this.state = state(initialState);
    this.storageKey = storageKey;
    this.loadFromStorage();
  }

  loadFromStorage() {
    const stored = localStorage.getItem(this.storageKey);
    if (stored) {
      const data = JSON.parse(stored);
      Object.assign(this.state, data);
    }
  }

  saveToStorage() {
    // Use toRaw for clean serialization
    const raw = toRaw(this.state);
    localStorage.setItem(this.storageKey, JSON.stringify(raw));
  }

  autoSave() {
    effect(() => {
      // Watch all state changes
      JSON.stringify(this.state);  // Track all properties

      // Save raw version
      this.saveToStorage();
    });
  }
}

// Usage
const appState = new StateManager(
  { theme: 'dark', user: null },
  'app-state'
);

appState.autoSave();
appState.state.theme = 'light';  // Auto-saves to localStorage

Example 2: API Service

js
class UserService {
  constructor() {
    this.cache = state({
      users: [],
      lastFetch: null
    });
  }

  async fetchUsers() {
    const response = await fetch('/api/users');
    const users = await response.json();

    this.cache.users = users;
    this.cache.lastFetch = Date.now();

    return users;
  }

  async createUser(userData) {
    // Extract raw data for API
    const raw = toRaw(userData);

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

    const newUser = await response.json();
    this.cache.users.push(newUser);

    return newUser;
  }

  async updateUser(user) {
    // Extract raw data for API
    const raw = toRaw(user);

    const response = await fetch(`/api/users/${raw.id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(raw)
    });

    return response.json();
  }

  exportUsers() {
    // Export clean JSON
    const raw = toRaw(this.cache.users);
    const json = JSON.stringify(raw, 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 = 'users.json';
    a.click();
  }
}

Example 3: State Snapshot System

js
class SnapshotManager {
  constructor(state) {
    this.state = state;
    this.snapshots = [];
    this.currentIndex = -1;
  }

  takeSnapshot() {
    // Remove snapshots after current index
    this.snapshots = this.snapshots.slice(0, this.currentIndex + 1);

    // Store raw copy
    const raw = toRaw(this.state);
    const snapshot = JSON.parse(JSON.stringify(raw));

    this.snapshots.push(snapshot);
    this.currentIndex++;

    console.log(`Snapshot ${this.currentIndex} taken`);
  }

  undo() {
    if (this.currentIndex <= 0) {
      console.log('Nothing to undo');
      return false;
    }

    this.currentIndex--;
    const snapshot = this.snapshots[this.currentIndex];

    // Restore state
    batch(() => {
      Object.keys(this.state).forEach(key => {
        delete this.state[key];
      });
      Object.assign(this.state, snapshot);
    });

    console.log(`Restored to snapshot ${this.currentIndex}`);
    return true;
  }

  redo() {
    if (this.currentIndex >= this.snapshots.length - 1) {
      console.log('Nothing to redo');
      return false;
    }

    this.currentIndex++;
    const snapshot = this.snapshots[this.currentIndex];

    // Restore state
    batch(() => {
      Object.keys(this.state).forEach(key => {
        delete this.state[key];
      });
      Object.assign(this.state, snapshot);
    });

    console.log(`Restored to snapshot ${this.currentIndex}`);
    return true;
  }
}

// Usage
const appState = state({ count: 0, name: 'App' });
const snapshots = new SnapshotManager(appState);

snapshots.takeSnapshot();  // Snapshot 0
appState.count = 5;
snapshots.takeSnapshot();  // Snapshot 1
appState.count = 10;
snapshots.takeSnapshot();  // Snapshot 2

snapshots.undo();  // Back to count: 5
snapshots.undo();  // Back to count: 0
snapshots.redo();  // Forward to count: 5

Example 4: Data Export/Import

js
class DataExporter {
  static exportToJSON(state, filename = 'data.json') {
    // Get raw data
    const raw = toRaw(state);

    // Pretty print JSON
    const json = JSON.stringify(raw, null, 2);

    // Create download
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();

    URL.revokeObjectURL(url);
  }

  static importFromJSON(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = (e) => {
        try {
          const data = JSON.parse(e.target.result);
          resolve(state(data));  // Make it reactive
        } catch (error) {
          reject(error);
        }
      };

      reader.onerror = reject;
      reader.readAsText(file);
    });
  }

  static clone(state) {
    // Deep clone without reactivity
    const raw = toRaw(state);
    const cloned = JSON.parse(JSON.stringify(raw));
    return state(cloned);  // Make new reactive instance
  }
}

// Usage
const myState = state({
  users: [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ],
  settings: { theme: 'dark' }
});

// Export
DataExporter.exportToJSON(myState, 'my-data.json');

// Import
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const imported = await DataExporter.importFromJSON(file);
  console.log('Imported:', imported);
});

// Clone
const cloned = DataExporter.clone(myState);
console.log(cloned !== myState);  // true (different instances)

Common Patterns

Pattern: Safe Serialization

js
function safeStringify(value) {
  try {
    const raw = toRaw(value);
    return JSON.stringify(raw);
  } catch (error) {
    console.error('Serialization failed:', error);
    return null;
  }
}

Pattern: Deep Clone

js
function deepClone(obj) {
  const raw = toRaw(obj);
  return JSON.parse(JSON.stringify(raw));
}

Pattern: Clean Logging

js
function logClean(label, value) {
  console.log(label, toRaw(value));
}

// VS

function logReactive(label, value) {
  console.log(label, value);  // Shows proxy wrapper
}

Pattern: External Library Integration

js
function useWithLibrary(state, libraryFn) {
  const raw = toRaw(state);
  return libraryFn(raw);
}

// Usage
const result = useWithLibrary(myState, moment);

Common Pitfalls

Pitfall #1: Modifying Raw Objects

Wrong:

js
const reactive = state({ count: 0 });
const raw = toRaw(reactive);

// Modifying raw doesn't trigger effects!
raw.count = 10;

// Effects won't run (raw is not reactive)

Correct:

js
const reactive = state({ count: 0 });

// Modify the reactive version
reactive.count = 10;  // Effects run

// Or get fresh raw after modification
const raw = toRaw(reactive);
console.log(raw.count);  // 10

Why? The raw object is just a plain object. Changes to it don't trigger effects.

Pitfall #2: Using toRaw in Effects

Wrong:

js
effect(() => {
  const raw = toRaw(app);
  console.log(raw.count);  // Won't track properly
});

app.count = 5;  // Effect might not re-run

Correct:

js
effect(() => {
  console.log(app.count);  // Tracks properly
});

app.count = 5;  // Effect re-runs

Why? Effects need to access reactive properties to track them. Using toRaw() breaks tracking.

Pitfall #3: Expecting Shared Reference

Wrong:

js
const reactive = state({ count: 0 });
const raw1 = toRaw(reactive);
const raw2 = toRaw(reactive);

raw1.count = 5;
console.log(raw2.count);  // 5 (they share the same object)

// But changes don't trigger effects!

Correct:

js
const reactive = state({ count: 0 });

// Modify reactive, not raw
reactive.count = 5;  // Effects trigger

// Then get raw if needed
const raw = toRaw(reactive);
console.log(raw.count);  // 5

Why? toRaw() returns the same underlying object, but modifying it bypasses reactivity.

Pitfall #4: Over-Using toRaw

Wrong:

js
// Using toRaw unnecessarily
function getCount(state) {
  const raw = toRaw(state);
  return raw.count;
}

Correct:

js
// Just access directly
function getCount(state) {
  return state.count;
}

Why? Use toRaw() only when you need a plain object, not for normal property access.

Summary

What is toRaw()?

toRaw() is an extraction utility that retrieves the original, non-reactive object from a reactive proxy, giving you back a plain JavaScript object.

Why use toRaw()?

  • Get plain objects for external APIs
  • Clean JSON serialization
  • LocalStorage/SessionStorage compatibility
  • Deep equality comparisons
  • External library integration
  • Clean debug logging

Key Points to Remember:

1️⃣ Extracts plain objects - Removes reactive wrapper 2️⃣ Breaks reactivity - Changes to raw won't trigger effects 3️⃣ Safe with any value - Returns value itself if not reactive 4️⃣ For external use - APIs, storage, serialization, libraries 5️⃣ Same object - Multiple toRaw() calls return same reference

Mental Model: Think of toRaw() as unwrapping a gift - you remove the decorative reactive wrapper to get the plain object inside.

Quick Reference:

js
// Basic usage
const reactive = state({ count: 0 });
const plain = toRaw(reactive);

console.log(isReactive(reactive));  // true
console.log(isReactive(plain));     // false

// Serialization
const json = JSON.stringify(toRaw(state));

// LocalStorage
localStorage.setItem('data', JSON.stringify(toRaw(state)));

// API requests
await fetch('/api/data', {
  method: 'POST',
  body: JSON.stringify(toRaw(state))
});

// Deep clone
const raw = toRaw(state);
const clone = JSON.parse(JSON.stringify(raw));

// Clean logging
console.log(toRaw(state));

Remember: toRaw() is your extraction tool for getting plain objects from reactive proxies. Use it when working with external APIs, storage, serialization, or libraries that expect plain JavaScript objects!