乐闻世界logo
搜索文章和话题

What are the differences between toJS, toJSON, and observable.shallow in MobX?

2月21日 15:50

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.

javascript
import { 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.

javascript
import { 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

javascript
class 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.

javascript
import { 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

javascript
class 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).

javascript
import { 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

FeaturetoJStoJSONobservable.shallow
PurposeConvert to plain JS objectConvert to JSON objectCreate shallow observable object
DepthDeep conversionDeep conversionOnly top-level observable
Return valuePlain JS objectJSON-serializable objectObservable object
ObservabilityNot observableNot observableObservable
Use casesAPI calls, storageSerialization, API responsePerformance 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

javascript
const shallowStore = observable.shallow({ items: [] }); // Won't trigger update shallowStore.items.push(1); // Will trigger update shallowStore.items = [1];

Pitfall 3: Confusing toJS and toJSON

javascript
const 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

javascript
class 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:

  1. toJS: Convert observable to plain JS object, used for API calls and storage
  2. toJSON: Convert observable to JSON object, used for serialization
  3. observable.shallow: Create shallow observable object, used for performance optimization

Using these tools correctly can build more efficient and maintainable MobX applications.

标签:Mobx