MobX provides three main reaction types: autorun, reaction, and when. Each has different use cases and characteristics, and understanding their differences is crucial for using MobX correctly.
1. autorun
Basic Usage
javascriptimport { autorun } from 'mobx'; const store = observable({ count: 0 }); autorun(() => { console.log(`Count is: ${store.count}`); }); store.count++; // Output: Count is: 1 store.count++; // Output: Count is: 2
Characteristics
- Immediate execution: autorun executes immediately upon creation
- Automatic tracking: Automatically tracks all observables accessed in the function
- Automatic re-execution: Automatically re-executes when tracked observables change
- No return value: Cannot return values, mainly used for side effects
Use Cases
- Logging
- Data persistence
- Syncing state to localStorage
- Sending analytics data
Example: Logging
javascriptautorun(() => { console.log('State changed:', toJS(store)); });
Example: Persisting to localStorage
javascriptautorun(() => { localStorage.setItem('appState', JSON.stringify(toJS(store))); });
2. reaction
Basic Usage
javascriptimport { reaction } from 'mobx'; const store = observable({ count: 0, name: 'John' }); reaction( () => store.count, // Tracking function (count, reaction) => { // Effect function console.log(`Count changed to: ${count}`); }, { fireImmediately: false } // Configuration options ); store.count++; // Output: Count changed to: 1 store.name = 'Jane'; // Won't trigger, only tracks count
Characteristics
- Delayed execution: By default, doesn't execute immediately
- Precise control: Can precisely specify which observables to track
- Compare changes: Can compare old and new values
- Configurable: Provides multiple configuration options
Configuration Options
javascriptreaction( () => store.count, (count, prevCount, reaction) => { console.log(`Count changed from ${prevCount} to ${count}`); }, { fireImmediately: true, // Execute immediately delay: 100, // Delay execution equals: (a, b) => a === b, // Custom comparison function name: 'myReaction' // Debug name } );
Use Cases
- Need precise control over tracking scope
- Need to compare old and new values
- Need delayed execution
- Complex side effect logic
Example: Search Debouncing
javascriptreaction( () => store.searchQuery, (query) => { // Delay search by 300ms debounce(() => { performSearch(query); }, 300); }, { delay: 300 } );
Example: Comparing Old and New Values
javascriptreaction( () => store.items, (items, prevItems) => { const added = items.filter(item => !prevItems.includes(item)); const removed = prevItems.filter(item => !items.includes(item)); console.log('Added:', added); console.log('Removed:', removed); }, { equals: comparer.structural } // Deep comparison );
3. when
Basic Usage
javascriptimport { when } from 'mobx'; const store = observable({ loaded: false, data: null }); when( () => store.loaded, // Condition function () => { // Effect function console.log('Data loaded:', store.data); } ); store.loaded = true; store.data = { name: 'John' }; // Output: Data loaded: { name: 'John' }
Characteristics
- One-time execution: Executes only once when condition is met
- Automatic cleanup: Automatically cleans up after execution
- Cancellable: Can be manually cancelled
- Returns disposer: Returns a cleanup function
Use Cases
- Wait for a condition to be met before executing
- Initialization logic
- One-time side effects
Example: Waiting for Data to Load
javascriptwhen( () => store.isLoaded, () => { initializeApp(); } );
Example: Cancellable when
javascriptconst dispose = when( () => store.userLoggedIn, () => { showWelcomeMessage(); } ); // If you need to cancel dispose();
Example: Timeout Handling
javascriptconst dispose = when( () => store.dataLoaded, () => { console.log('Data loaded successfully'); } ); // Cancel after 5 seconds setTimeout(() => { dispose(); console.log('Loading timeout'); }, 5000);
Comparison of the Three
| Feature | autorun | reaction | when |
|---|---|---|---|
| Execution timing | Immediate | Delayed (default) | When condition is met |
| Execution count | Multiple | Multiple | Once |
| Tracking scope | Auto-track all dependencies | Precise tracking scope | Only tracks condition |
| Return value | None | disposer | disposer |
| Use cases | Logging, persistence | Complex side effects, comparing values | Initialization, one-time operations |
Selection Guide
Use autorun when:
- Need immediate execution
- Need to track all dependencies
- For simple side effects
- Don't need to compare old and new values
javascriptautorun(() => { document.title = store.pageTitle; });
Use reaction when:
- Need precise control over tracking scope
- Need to compare old and new values
- Need delayed execution
- Need complex side effect logic
javascriptreaction( () => store.userId, (userId, prevUserId) => { if (userId !== prevUserId) { loadUserData(userId); } } );
Use when when:
- Need to wait for a condition
- Only need to execute once
- For initialization logic
- Need cancellable operations
javascriptwhen( () => store.initialized, () => { startApp(); } );
Performance Considerations
1. Avoid Over-tracking
javascript// Bad practice: autorun tracks too much autorun(() => { console.log(store.user.name, store.user.email, store.user.age); }); // Good practice: reaction tracks precisely reaction( () => store.user.name, (name) => { console.log(name); } );
2. Clean Up Promptly
javascript// Clean up when component unmounts useEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => { dispose(); // Clean up reaction }; }, []);
3. Use comparer to Optimize Comparison
javascriptimport { comparer } from 'mobx'; reaction( () => store.items, (items) => { console.log('Items changed'); }, { equals: comparer.structural } // Deep comparison, avoid unnecessary updates );
Common Pitfalls
1. Side Effects in reaction
javascript// Bad practice: Side effects in tracking function reaction( () => { console.log('Side effect!'); // Shouldn't be in tracking function return store.count; }, (count) => { console.log(count); } ); // Good practice: Tracking function should be pure reaction( () => store.count, (count) => { console.log('Side effect:', count); // Side effects in effect function } );
2. Forgetting to Clean Up reaction
javascript// Bad practice: Forgetting to clean up useEffect(() => { autorun(() => { console.log(store.count); }); // No cleanup function }, []); // Good practice: Clean up reaction useEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => dispose(); }, []);
3. Abusing autorun
javascript// Bad practice: Using autorun for one-time operations autorun(() => { if (store.initialized) { initializeApp(); // Will execute multiple times } }); // Good practice: Use when for one-time operations when( () => store.initialized, () => { initializeApp(); // Executes only once } );
Summary
Understanding the differences and use cases of autorun, reaction, and when is key to mastering MobX:
- autorun: For simple, immediately executing side effects
- reaction: For complex side effects that need precise control and value comparison
- when: For one-time operations that wait for a condition to be met
Choosing and using these reaction types correctly can build more efficient and maintainable MobX applications.