MobX provides several tools for handling state, including toJS, toJSON, and observable.shallow. Understanding their differences and use cases is crucial for using MobX correctly.
1. toJS
Basic Usage
toJS deeply converts observable objects to plain JavaScript objects.
javascriptimport { observable, toJS } from 'mobx'; const store = observable({ user: { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }, items: [1, 2, 3] }); // Convert to plain object const plainObject = toJS(store); console.log(plainObject); // { // user: { // name: 'John', // age: 30, // address: { city: 'New York', country: 'USA' } // }, // items: [1, 2, 3] // } // plainObject is no longer observable console.log(isObservable(plainObject)); // false console.log(isObservable(plainObject.user)); // false console.log(isObservable(plainObject.items)); // false
Use Cases
- Send observable objects to API
- Store observable objects to localStorage
- Pass observable objects to libraries that don't support observables
- Debug state
Example: Sending to API
javascript@action async saveData() { const plainData = toJS(this.data); await api.saveData(plainData); }
Example: Storing to localStorage
javascript@action saveToLocalStorage() { const plainState = toJS(this.state); localStorage.setItem('appState', JSON.stringify(plainState)); }
2. toJSON
Basic Usage
toJSON converts observable objects to JSON-serializable objects.
javascriptimport { observable, toJSON } from 'mobx'; const store = observable({ user: { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }, items: [1, 2, 3] }); // Convert to JSON object const jsonObject = toJSON(store); console.log(jsonObject); // { // user: { // name: 'John', // age: 30, // address: { city: 'New York', country: 'USA' } // }, // items: [1, 2, 3] // } // Can be directly serialized to JSON const jsonString = JSON.stringify(store); console.log(jsonString); // {"user":{"name":"John","age":30,"address":{"city":"New York","country":"USA"}},"items":[1,2,3]}
Custom toJSON
javascriptclass User { @observable name = 'John'; @observable password = 'secret'; @observable email = 'john@example.com'; toJSON() { return { name: this.name, email: this.email // Doesn't include password }; } } const user = new User(); const json = JSON.stringify(user); console.log(json); // {"name":"John","email":"john@example.com"}
Use Cases
- Serialize observable objects to JSON
- Send data to server
- Store data to database
- Create API responses
3. observable.shallow
Basic Usage
observable.shallow creates shallow observable objects where only top-level properties are observable.
javascriptimport { observable } from 'mobx'; // Deep observable (default) const deepStore = observable({ user: { name: 'John', age: 30 }, items: [1, 2, 3] }); // Shallow observable const shallowStore = observable.shallow({ user: { name: 'John', age: 30 }, items: [1, 2, 3] }); // Nested objects in deepStore are also observable deepStore.user.name = 'Jane'; // Will trigger update deepStore.items.push(4); // Will trigger update // Nested objects in shallowStore are not observable shallowStore.user.name = 'Jane'; // Won't trigger update shallowStore.items.push(4); // Won't trigger update // But top-level property changes will trigger update shallowStore.user = { name: 'Jane', age: 30 }; // Will trigger update shallowStore.items = [1, 2, 3, 4]; // Will trigger update
Use Cases
- Performance optimization: reduce dependencies to track
- Avoid performance issues from deep tracking
- Only need to track top-level changes
- Handle large data structures
Example: Large Array
javascriptclass Store { @observable.shallow items = []; constructor() { makeAutoObservable(this); } @action loadItems = async () => { const data = await fetch('/api/items').then(r => r.json()); this.items = data; // Only track array replacement }; }
4. observable.deep
Basic Usage
observable.deep creates deeply observable objects where all nested properties are observable (this is the default behavior).
javascriptimport { observable } from 'mobx'; const deepStore = observable.deep({ user: { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }, items: [1, 2, 3] }); // All nested properties are observable deepStore.user.name = 'Jane'; // Will trigger update deepStore.user.address.city = 'Boston'; // Will trigger update deepStore.items.push(4); // Will trigger update
5. Comparison Summary
| Feature | toJS | toJSON | observable.shallow |
|---|---|---|---|
| Purpose | Convert to plain JS object | Convert to JSON object | Create shallow observable object |
| Depth | Deep conversion | Deep conversion | Only top-level observable |
| Return value | Plain JS object | JSON-serializable object | Observable object |
| Observability | Not observable | Not observable | Observable |
| Use cases | API calls, storage | Serialization, API response | Performance optimization |
6. Performance Considerations
Using shallow for Performance Optimization
javascript// Bad practice: deeply observable large array class BadStore { @observable items = []; // May have thousands of elements } // Good practice: shallow observable class GoodStore { @observable.shallow items = []; @action loadItems = async () => { const data = await fetch('/api/items').then(r => r.json()); this.items = data; // Only track array replacement }; }
Avoid Frequent toJS Calls
javascript// Bad practice: Frequent toJS calls @observer class BadComponent extends React.Component { render() { const plainData = toJS(store.data); // Called every render return <div>{plainData.length}</div>; } } // Good practice: Cache result or use observable directly @observer class GoodComponent extends React.Component { render() { return <div>{store.data.length}</div>; // Use observable directly } }
7. Common Pitfalls
Pitfall 1: Calling toJS in computed
javascript// Bad practice @computed get badComputed() { const plainData = toJS(this.data); return plainData.filter(item => item.active); } // Good practice @computed get goodComputed() { return this.data.filter(item => item.active); }
Pitfall 2: Forgetting shallow Limitations
javascriptconst shallowStore = observable.shallow({ items: [] }); // Won't trigger update shallowStore.items.push(1); // Will trigger update shallowStore.items = [1];
Pitfall 3: Confusing toJS and toJSON
javascriptconst store = observable({ user: { name: 'John' } }); // toJS returns plain object const plain = toJS(store); console.log(plain instanceof Object); // true // toJSON returns JSON-serializable object const json = toJSON(store); console.log(JSON.stringify(json)); // {"user":{"name":"John"}}
8. Best Practices
1. Choose Observable Depth Based on Needs
javascript// Small data structures: use deep observable const smallStore = observable({ config: { theme: 'dark', language: 'en' } }); // Large data structures: use shallow observable const largeStore = observable.shallow({ items: [] // May have thousands of elements });
2. Use toJS Only When Needed
javascript// Only use when sending to API @action async sendData() { const plainData = toJS(this.data); await api.sendData(plainData); } // Use observable directly in components @observer const Component = () => { return <div>{store.data.length}</div>; };
3. Customize toJSON to Control Serialization
javascriptclass User { @observable id = 1; @observable name = 'John'; @observable password = 'secret'; toJSON() { return { id: this.id, name: this.name // Doesn't include sensitive information }; } }
Summary
Understanding the differences and use cases of toJS, toJSON, and observable.shallow:
- toJS: Convert observable to plain JS object, used for API calls and storage
- toJSON: Convert observable to JSON object, used for serialization
- observable.shallow: Create shallow observable object, used for performance optimization
Using these tools correctly can build more efficient and maintainable MobX applications.