5月30日 01:39
How to handle asynchronous operations in MobX?
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