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

How to handle asynchronous operations in MobX?

2月19日 17:56

Handling asynchronous operations in MobX requires special attention, as async operations may modify state at multiple moments. Here are the best practices for handling async operations:

1. Wrap Async Operations with Actions

Basic Usage

javascript
import { observable, action, runInAction } from 'mobx'; class Store { @observable data = null; @observable loading = false; @observable error = null; @action async fetchData() { this.loading = true; this.error = null; try { const response = await fetch('/api/data'); const data = await response.json(); // Wrap state updates with runInAction runInAction(() => { this.data = data; }); } catch (error) { runInAction(() => { this.error = error.message; }); } finally { runInAction(() => { this.loading = false; }); } } }

Why runInAction is Needed

In async operations, state updates may not be in the action's execution context. Using runInAction ensures all state updates are done within an action.

2. Using flow for Async Operations

MobX provides the flow utility function for more elegant async operation handling:

javascript
import { observable, flow } from 'mobx'; class Store { @observable data = null; @observable loading = false; @observable error = null; fetchData = flow(function* () { this.loading = true; this.error = null; try { const response = yield fetch('/api/data'); const data = yield response.json(); this.data = data; } catch (error) { this.error = error.message; } finally { this.loading = false; } }); }

Advantages of flow

  • Automatically handles async operations
  • Code is more concise, similar to synchronous code
  • Automatically wraps state updates
  • Better error handling

3. Handling Multiple Async Operations

Parallel Execution

javascript
@action async fetchMultipleData() { this.loading = true; try { const [users, posts] = await Promise.all([ fetch('/api/users').then(r => r.json()), fetch('/api/posts').then(r => r.json()) ]); runInAction(() => { this.users = users; this.posts = posts; }); } finally { runInAction(() => { this.loading = false; }); } }

Sequential Execution

javascript
fetchDataSequential = flow(function* () { this.loading = true; try { const userResponse = yield fetch('/api/user'); const user = yield userResponse.json(); this.user = user; const postsResponse = yield fetch(`/api/users/${user.id}/posts`); const posts = yield postsResponse.json(); this.posts = posts; } finally { this.loading = false; } });

4. Canceling Async Operations

Using AbortController

javascript
class Store { @observable data = null; @observable loading = false; abortController = null; @action async fetchData() { // Cancel previous request if (this.abortController) { this.abortController.abort(); } this.abortController = new AbortController(); this.loading = true; try { const response = await fetch('/api/data', { signal: this.abortController.signal }); const data = await response.json(); runInAction(() => { this.data = data; }); } catch (error) { if (error.name !== 'AbortError') { runInAction(() => { this.error = error.message; }); } } finally { runInAction(() => { this.loading = false; }); } } @action cancelRequest() { if (this.abortController) { this.abortController.abort(); } } }

5. Error Handling and Retry

Basic Error Handling

javascript
@action async fetchDataWithRetry(maxRetries = 3) { this.loading = true; let retries = 0; while (retries < maxRetries) { try { const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; this.error = null; }); return; } catch (error) { retries++; if (retries >= maxRetries) { runInAction(() => { this.error = error.message; }); throw error; } // Wait before retrying await new Promise(resolve => setTimeout(resolve, 1000 * retries)); } finally { if (retries >= maxRetries) { runInAction(() => { this.loading = false; }); } } } }

6. Using Async Operations in React Components

javascript
import React, { useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { useLocalObservable } from 'mobx-react-lite'; const DataComponent = observer(() => { const store = useLocalObservable(() => ({ data: null, loading: false, error: null, fetchData: flow(function* () { this.loading = true; this.error = null; try { const response = yield fetch('/api/data'); const data = yield response.json(); this.data = data; } catch (error) { this.error = error.message; } finally { this.loading = false; } }) })); useEffect(() => { store.fetchData(); }, []); if (store.loading) return <div>Loading...</div>; if (store.error) return <div>Error: {store.error}</div>; return <div>{JSON.stringify(store.data)}</div>; });

7. Best Practices Summary

1. Always Modify State in Actions

  • Use @action decorator
  • Use runInAction to wrap async state updates
  • Use flow for complex async flows

2. Handle Loading State Correctly

  • Set loading before starting async operation
  • Clear loading after operation completes
  • Ensure loading is cleared in finally block

3. Handle Errors

  • Catch all possible errors
  • Store error messages in observables
  • Display error messages in UI

4. Cancel Unnecessary Requests

  • Use AbortController to cancel requests
  • Cancel requests when component unmounts
  • Avoid memory leaks

5. Use flow to Simplify Code

  • For complex async flows, prefer using flow
  • flow makes code more readable and maintainable
  • Automatically handles action wrapping

8. Common Pitfalls

1. Forgetting to Use runInAction

javascript
// Wrong @action async fetchData() { const data = await fetch('/api/data').then(r => r.json()); this.data = data; // Not in action } // Correct @action async fetchData() { const data = await fetch('/api/data').then(r => r.json()); runInAction(() => { this.data = data; }); }

2. Creating Multiple Actions in Loops

javascript
// Wrong @action async fetchMultiple() { const items = await fetch('/api/items').then(r => r.json()); items.forEach(item => { runInAction(() => { // Multiple runInAction calls this.items.push(item); }); }); } // Correct @action async fetchMultiple() { const items = await fetch('/api/items').then(r => r.json()); runInAction(() => { this.items.push(...items); }); }

3. Forgetting to Clean Up Side Effects

javascript
// Wrong useEffect(() => { store.fetchData(); // Request may still be in progress when component unmounts }, []); // Correct useEffect(() => { const task = store.fetchData(); return () => { task.cancel(); // Cancel request }; }, []);

Summary

Handling async operations in MobX requires following these principles:

  1. Always modify state in actions
  2. Use runInAction or flow to wrap async state updates
  3. Handle loading state and errors correctly
  4. Cancel unnecessary requests
  5. Follow best practices to avoid common pitfalls

Properly handling async operations is key to building reliable MobX applications.

标签:Mobx