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:
// 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 triggeredThat'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
// 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:
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 proxyThis 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:
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:
// 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():
- Checks if value exists
- Checks if value has the
RAWsymbol - Returns the original object if found
- 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 reactiveWhat 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():
// 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:
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 reactiveSerialization
Perfect for JSON operations:
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
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
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
function deepEqual(a, b) {
const rawA = toRaw(a);
const rawB = toRaw(b);
return JSON.stringify(rawA) === JSON.stringify(rawB);
}4. External Library Integration
function processWithLibrary(data) {
// External libraries may not handle proxies
const raw = toRaw(data);
return externalLibrary.process(raw);
}5. Debug Logging
function logState(state, label) {
console.log(label, toRaw(state));
// Clean output without proxy wrapper
}❌ Not Needed
1. Normal State Access
// 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
// 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
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 localStorageExample 2: API Service
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
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: 5Example 4: Data Export/Import
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
function safeStringify(value) {
try {
const raw = toRaw(value);
return JSON.stringify(raw);
} catch (error) {
console.error('Serialization failed:', error);
return null;
}
}Pattern: Deep Clone
function deepClone(obj) {
const raw = toRaw(obj);
return JSON.parse(JSON.stringify(raw));
}Pattern: Clean Logging
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
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:
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:
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); // 10Why? The raw object is just a plain object. Changes to it don't trigger effects.
Pitfall #2: Using toRaw in Effects
❌ Wrong:
effect(() => {
const raw = toRaw(app);
console.log(raw.count); // Won't track properly
});
app.count = 5; // Effect might not re-run✅ Correct:
effect(() => {
console.log(app.count); // Tracks properly
});
app.count = 5; // Effect re-runsWhy? Effects need to access reactive properties to track them. Using toRaw() breaks tracking.
Pitfall #3: Expecting Shared Reference
❌ Wrong:
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:
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); // 5Why? toRaw() returns the same underlying object, but modifying it bypasses reactivity.
Pitfall #4: Over-Using toRaw
❌ Wrong:
// Using toRaw unnecessarily
function getCount(state) {
const raw = toRaw(state);
return raw.count;
}✅ Correct:
// 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:
// 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!