MobX is a powerful state management library, but developers may encounter some common issues during use. Understanding these problems and their solutions can help developers use MobX more effectively.
1. Component Not Updating
Problem Description
Component is wrapped with observer but doesn't update when state changes.
Common Causes and Solutions
Cause 1: Accessing Plain Object Instead of Observable
javascript// Wrong const store = { count: 0 }; @observer class Counter extends React.Component { render() { return <div>{store.count}</div>; // Won't update } } // Correct import { observable } from 'mobx'; const store = observable({ count: 0 }); @observer class Counter extends React.Component { render() { return <div>{store.count}</div>; // Will update } }
Cause 2: Modifying State Outside Action (MobX 6)
javascript// Wrong (MobX 6) class Store { @observable count = 0; increment() { this.count++; // Not in action, will throw error } } // Correct class Store { @observable count = 0; @action increment() { this.count++; // In action } }
Cause 3: Creating New Objects in render
javascript// Wrong @observer class Component extends React.Component { render() { const style = { color: 'red' }; // Creates new object every render return <div style={style}>{store.count}</div>; } } // Correct const style = { color: 'red' }; // Define outside component @observer class Component extends React.Component { render() { return <div style={style}>{store.count}</div>; } }
2. Performance Issues
Problem Description
Application performance degrades, components re-render frequently.
Common Causes and Solutions
Cause 1: Over-tracking
javascript// Wrong: Accessing observable in loop @observer class List extends React.Component { render() { return ( <div> {store.items.map(item => ( <div key={item.id}> {item.name} - {item.value} - {item.description} </div> ))} </div> ); } } // Correct: Use computed to preprocess data class Store { @observable items = []; @computed get itemDisplayData() { return this.items.map(item => ({ id: item.id, display: `${item.name} - ${item.value} - ${item.description}` })); } } @observer class List extends React.Component { render() { return ( <div> {store.itemDisplayData.map(item => ( <div key={item.id}>{item.display}</div> ))} </div> ); } }
Cause 2: Component Depends on Too Much State
javascript// Wrong: Component depends on too much state @observer class Dashboard extends React.Component { render() { return ( <div> <UserInfo /> <Settings /> <DataCount /> </div> ); } } // Correct: Split into multiple components @observer class UserInfo extends React.Component { render() { return ( <div> <div>{store.user.name}</div> <div>{store.user.email}</div> </div> ); } }
3. Memory Leaks
Problem Description
Reaction continues to run after component unmounts, causing memory leaks.
Solution
javascript// Wrong: Forgot to clean up reaction useEffect(() => { autorun(() => { console.log(store.count); }); }, []); // Correct: Clean up reaction useEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => { dispose(); // Clean up reaction }; }, []);
4. Async Operation Issues
Problem Description
State modifications in async operations don't take effect.
Solution
javascript// Wrong: State modification in async operation @action async fetchData() { this.loading = true; const data = await fetch('/api/data').then(r => r.json()); this.data = data; // Not in action this.loading = false; // Not in action } // Correct: Use runInAction @action async fetchData() { this.loading = true; try { const data = await fetch('/api/data').then(r => r.json()); runInAction(() => { this.data = data; }); } finally { runInAction(() => { this.loading = false; }); } } // Or use flow fetchData = flow(function* () { this.loading = true; try { const response = yield fetch('/api/data'); const data = yield response.json(); this.data = data; } finally { this.loading = false; } });
5. computed Not Updating
Problem Description
computed property doesn't update as expected.
Common Causes and Solutions
Cause 1: Side Effects in computed
javascript// Wrong: Side effects in computed @computed get badComputed() { console.log('Side effect!'); // Shouldn't be in computed fetch('/api/data'); // Shouldn't be in computed return this.data; } // Correct: computed should be pure functions @computed get goodComputed() { return this.data.filter(item => item.active); }
Cause 2: Dependencies Not Tracked Correctly
javascript// Wrong: Dependencies not tracked correctly @computed get badComputed() { const data = this.data; // Not used in return value return this.items.length; } // Correct: Dependencies tracked correctly @computed get goodComputed() { return this.data.length + this.items.length; }
6. Circular Dependencies
Problem Description
Circular dependencies between stores cause infinite loops or performance issues.
Solution
javascript// Wrong: Circular dependencies class StoreA { @observable value = 0; @computed get doubled() { return storeB.value * 2; } } class StoreB { @observable value = 0; @computed get doubled() { return storeA.value * 2; } } // Correct: Avoid circular dependencies class Store { @observable valueA = 0; @observable valueB = 0; @computed get doubledA() { return this.valueA * 2; } @computed get doubledB() { return this.valueB * 2; } }
7. Decorators Not Working
Problem Description
Errors occur or decorators don't take effect when using decorators.
Solution
Ensure Correct Configuration
javascript// package.json { "babel": { "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose": true }] ] } }
Or Use makeObservable
javascript// Without decorators class Store { count = 0; constructor() { makeObservable(this, { count: observable }); } }
8. TypeScript Type Errors
Problem Description
Type errors occur when using TypeScript.
Solution
javascript// Wrong: No type parameter class Store { count = 0; constructor() { makeObservable(this, { count: observable }); } } // Correct: Use type parameter class Store { count: number = 0; constructor() { makeObservable<Store>(this, { count: observable }); } }
9. Array Operation Issues
Problem Description
Array operations don't trigger updates.
Solution
javascript// Wrong: Reassign entire array @action badAddItem(item) { this.items = [...this.items, item]; } // Correct: Use array methods @action goodAddItem(item) { this.items.push(item); } // Or use replace @action replaceItems(newItems) { this.items.replace(newItems); }
10. Difficult to Debug
Problem Description
Difficult to track state changes and dependencies.
Solution
Use trace
javascriptimport { trace } from 'mobx'; // Trace computed trace(store.fullName); // Trace component rendering @observer class MyComponent extends React.Component { render() { trace(true); // Trace component rendering return <div>{store.count}</div>; } }
Use MobX DevTools
javascriptimport { configure } from 'mobx'; configure({ // Enable debug mode useProxies: 'ifavailable', isolateGlobalState: true });
Best Practices Summary
- Always modify state in actions (MobX 6)
- Use observer to wrap components that need to respond to state changes
- Avoid creating new objects in render
- Use computed to optimize calculation logic
- Clean up reactions and side effects promptly
- Handle async operations correctly
- Avoid circular dependencies
- Use trace and DevTools for debugging
- Reasonably split components to reduce dependencies
- Follow MobX best practices
Following these best practices can avoid most common MobX issues and build stable, efficient applications.