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
javascriptimport { 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:
javascriptimport { 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
javascriptfetchDataSequential = 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
javascriptclass 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
javascriptimport 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
@actiondecorator - Use
runInActionto wrap async state updates - Use
flowfor 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:
- Always modify state in actions
- Use runInAction or flow to wrap async state updates
- Handle loading state and errors correctly
- Cancel unnecessary requests
- Follow best practices to avoid common pitfalls
Properly handling async operations is key to building reliable MobX applications.