In MobX, asynchronous operations require special attention because state changes must occur within actions. MobX provides multiple ways to handle asynchronous operations.
Ways to Handle Asynchronous Operations
1. Using runInAction
javascriptimport { observable, action, runInAction } from 'mobx'; class UserStore { @observable users = []; @observable loading = false; @observable error = null; @action async fetchUsers() { this.loading = true; this.error = null; try { const response = await fetch('/api/users'); const data = await response.json(); runInAction(() => { this.users = data; this.loading = false; }); } catch (error) { runInAction(() => { this.error = error.message; this.loading = false; }); } } }
2. Using async action
javascriptimport { observable, action } from 'mobx'; class UserStore { @observable users = []; @observable loading = false; @observable error = null; @action.bound async fetchUsers() { this.loading = true; this.error = null; try { const response = await fetch('/api/users'); const data = await response.json(); this.users = data; this.loading = false; } catch (error) { this.error = error.message; this.loading = false; } } }
3. Using flow
javascriptimport { observable, flow } from 'mobx'; class UserStore { @observable users = []; @observable loading = false; @observable error = null; fetchUsers = flow(function* () { this.loading = true; this.error = null; try { const response = yield fetch('/api/users'); const data = yield response.json(); this.users = data; this.loading = false; } catch (error) { this.error = error.message; this.loading = false; } }); }
Detailed Comparison
runInAction
Pros:
- High flexibility, can be used anywhere
- Suitable for handling complex async logic
- Can precisely control timing of state updates
Cons:
- Need to manually wrap state update code
- Code structure may not be clear enough
Use cases:
- Need to update state at different stages of async operations
- Complex async logic
- Need precise control over state update timing
javascript@action async complexOperation() { this.loading = true; try { const data1 = await this.fetchData1(); runInAction(() => { this.data1 = data1; }); const data2 = await this.fetchData2(data1.id); runInAction(() => { this.data2 = data2; }); const result = await this.processData(data1, data2); runInAction(() => { this.result = result; this.loading = false; }); } catch (error) { runInAction(() => { this.error = error.message; this.loading = false; }); } }
async action
Pros:
- Clear code structure
- Automatically handles state updates
- No need to manually wrap code
Cons:
- Lower flexibility
- All state updates in the same action
Use cases:
- Simple async operations
- No need for precise control over state update timing
- Code structure priority scenarios
javascript@action.bound async simpleFetch() { this.loading = true; try { const response = await fetch('/api/data'); const data = await response.json(); this.data = data; this.loading = false; } catch (error) { this.error = error.message; this.loading = false; } }
flow
Pros:
- Clearest code structure
- Automatically handles state updates
- Supports cancellation
- Better error handling
Cons:
- Need to learn generator syntax
- Compatibility issues (requires polyfill)
Use cases:
- Complex async flows
- Scenarios requiring cancellation
- Need better error handling
javascriptfetchUsers = flow(function* () { this.loading = true; this.error = null; try { const response = yield fetch('/api/users'); const data = yield response.json(); this.users = data; this.loading = false; } catch (error) { this.error = error.message; this.loading = false; } }); // Can cancel flow const fetchTask = store.fetchUsers(); fetchTask.cancel();
Best Practices
1. Uniformly use async action
javascriptclass ApiStore { @observable data = null; @observable loading = false; @observable error = null; @action.bound async fetchData(url) { this.loading = true; this.error = null; try { const response = await fetch(url); const data = await response.json(); this.data = data; this.loading = false; } catch (error) { this.error = error.message; this.loading = false; } } }
2. Use flow for complex flows
javascriptclass OrderStore { @observable orders = []; @observable loading = false; @observable error = null; createOrder = flow(function* (orderData) { this.loading = true; this.error = null; try { // Validate order const validated = yield this.validateOrder(orderData); // Create order const order = yield this.createOrderApi(validated); // Pay order const paid = yield this.payOrder(order.id); // Update state this.orders.push(paid); this.loading = false; return paid; } catch (error) { this.error = error.message; this.loading = false; throw error; } }); }
3. Use runInAction for step-by-step updates
javascriptclass UploadStore { @observable progress = 0; @observable files = []; @observable uploading = false; @action async uploadFiles(files) { this.uploading = true; this.progress = 0; try { for (let i = 0; i < files.length; i++) { const file = files[i]; await this.uploadFile(file); runInAction(() => { this.files.push(file); this.progress = ((i + 1) / files.length) * 100; }); } runInAction(() => { this.uploading = false; }); } catch (error) { runInAction(() => { this.uploading = false; }); throw error; } } }
4. Use reaction for automatic loading
javascriptclass DataStore { @observable userId = null; @observable userData = null; @observable loading = false; constructor() { reaction( () => this.userId, (userId) => { if (userId) { this.loadUserData(userId); } } ); } @action.bound async loadUserData(userId) { this.loading = true; try { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); this.userData = data; this.loading = false; } catch (error) { this.loading = false; } } }
5. Error handling and retry
javascriptclass Store { @observable data = null; @observable loading = false; @observable error = null; @observable retryCount = 0; @action.bound async fetchDataWithRetry(url, maxRetries = 3) { this.loading = true; this.error = null; for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); runInAction(() => { this.data = data; this.loading = false; this.retryCount = 0; }); return data; } catch (error) { runInAction(() => { this.retryCount = i + 1; }); if (i === maxRetries - 1) { runInAction(() => { this.error = error.message; this.loading = false; }); throw error; } // Wait before retry await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); } } } }
Common Mistakes
1. Directly modifying state in async functions
javascript// ❌ Wrong: directly modifying state in async function @action async fetchData() { this.loading = true; const response = await fetch('/api/data'); const data = await response.json(); this.data = data; // Not in action this.loading = false; } // ✅ Correct: use runInAction or async action @action async fetchData() { this.loading = true; const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; this.loading = false; }); } // Or @action.bound async fetchData() { this.loading = true; const response = await fetch('/api/data'); const data = await response.json(); this.data = data; this.loading = false; }
2. Forgetting to handle errors
javascript// ❌ Wrong: forgetting to handle errors @action async fetchData() { this.loading = true; const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; this.loading = false; }); } // ✅ Correct: handle errors @action async fetchData() { this.loading = true; this.error = null; try { const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; this.loading = false; }); } catch (error) { runInAction(() => { this.error = error.message; this.loading = false; }); } }
3. Forgetting to reset loading state
javascript// ❌ Wrong: forgetting to reset loading state @action async fetchData() { this.loading = true; try { const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; }); } catch (error) { runInAction(() => { this.error = error.message; }); } } // ✅ Correct: reset loading state in all branches @action async fetchData() { this.loading = true; try { const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; this.loading = false; }); } catch (error) { runInAction(() => { this.error = error.message; this.loading = false; }); } }
Performance Optimization
1. Use debounce
javascriptimport { debounce } from 'lodash'; class SearchStore { @observable query = ''; @observable results = []; @observable loading = false; constructor() { this.debouncedSearch = debounce(this.performSearch.bind(this), 300); } @action setQuery(query) { this.query = query; this.debouncedSearch(); } @action.bound async performSearch() { if (this.query.length < 2) { this.results = []; return; } this.loading = true; try { const response = await fetch(`/api/search?q=${this.query}`); const data = await response.json(); this.results = data; this.loading = false; } catch (error) { this.loading = false; } } }
2. Use requestAnimationFrame to optimize UI updates
javascript@action async loadData() { this.loading = true; const data = await this.fetchData(); // Use requestAnimationFrame to optimize UI updates requestAnimationFrame(() => { runInAction(() => { this.data = data; this.loading = false; }); }); }
Summary
- Use async action for simple async operations
- Use runInAction for scenarios requiring precise control over state update timing
- Use flow for complex async flows
- Always handle errors and reset loading state
- Use reaction for automatic loading
- Use debounce to optimize performance
- Use requestAnimationFrame to optimize UI updates