In MobX, reaction is a mechanism for handling side effects that automatically executes specified functions when observable state changes. reaction is similar to React's useEffect but more flexible and efficient.
Reaction Types
1. autorun
Automatically tracks dependencies and executes immediately when dependencies change, suitable for scenarios requiring immediate execution.
javascriptimport { observable, autorun } from 'mobx'; class TodoStore { @observable todos = []; constructor() { autorun(() => { console.log('Total todos:', this.todos.length); // Save to localStorage localStorage.setItem('todos', JSON.stringify(this.todos)); }); } }
2. reaction
Provides finer-grained control, allowing specification of tracked data and execution functions, suitable for scenarios requiring control over execution timing.
javascriptimport { observable, reaction } from 'mobx'; class TodoStore { @observable todos = []; @observable filter = 'all'; constructor() { reaction( () => this.todos.length, // Tracked data (length) => { console.log('Todo count changed:', length); }, { fireImmediately: false } // Configuration options ); } }
3. when
Executes once when a condition is met, then automatically cleans up, suitable for one-time operations.
javascriptimport { observable, when } from 'mobx'; class TodoStore { @observable todos = []; @observable loading = false; constructor() { when( () => !this.loading && this.todos.length === 0, () => { console.log('Ready to load todos'); this.loadTodos(); } ); } @action loadTodos() { this.loading = true; // Load data... } }
Reaction Configuration Options
1. fireImmediately
Whether to execute immediately once.
javascriptreaction( () => this.filter, (filter) => { console.log('Current filter:', filter); }, { fireImmediately: true } // Execute immediately once );
2. delay
Delay execution, debounce effect.
javascriptreaction( () => this.searchQuery, (query) => { this.performSearch(query); }, { delay: 300 } // Delay execution by 300ms );
3. equals
Custom comparison function to determine whether to trigger reaction.
javascriptreaction( () => this.items, (items) => { console.log('Items changed'); }, { equals: (a, b) => { return a.length === b.length && a.every(item => b.includes(item)); } } );
4. name
Set a name for reaction to facilitate debugging.
javascriptreaction( () => this.todos, (todos) => { console.log('Todos updated'); }, { name: 'saveTodosToLocalStorage' } );
Reaction Use Cases
1. Data Persistence
javascriptclass TodoStore { @observable todos = []; constructor() { // Load from localStorage this.todos = JSON.parse(localStorage.getItem('todos') || '[]'); // Save to localStorage autorun(() => { localStorage.setItem('todos', JSON.stringify(this.todos)); }); } }
2. Logging
javascriptclass Store { @observable user = null; @observable actions = []; constructor() { reaction( () => this.user, (user) => { console.log('User changed:', user); this.actions.push({ type: 'USER_CHANGE', user, timestamp: Date.now() }); } ); } }
3. API Calls
javascriptclass ProductStore { @observable categoryId = null; @observable products = []; @observable loading = false; constructor() { reaction( () => this.categoryId, async (categoryId) => { if (categoryId) { this.loading = true; try { const response = await fetch(`/api/products?category=${categoryId}`); const data = await response.json(); runInAction(() => { this.products = data; this.loading = false; }); } catch (error) { runInAction(() => { this.loading = false; }); } } } ); } }
4. Search Debouncing
javascriptclass SearchStore { @observable query = ''; @observable results = []; @observable loading = false; constructor() { reaction( () => this.query, async (query) => { if (query.length > 2) { this.loading = true; try { const response = await fetch(`/api/search?q=${query}`); const data = await response.json(); runInAction(() => { this.results = data; this.loading = false; }); } catch (error) { runInAction(() => { this.loading = false; }); } } }, { delay: 300 } // Debounce 300ms ); } }
5. Conditional Initialization
javascriptclass AppStore { @observable initialized = false; @observable user = null; constructor() { when( () => this.initialized, () => { this.loadUserData(); } ); } @action loadUserData() { // Load user data } }
Reaction Cleanup
1. Use dispose to cleanup
javascriptclass Component { disposer = null; componentDidMount() { this.disposer = reaction( () => this.store.todos, (todos) => { console.log('Todos changed:', todos); } ); } componentWillUnmount() { if (this.disposer) { this.disposer(); // Cleanup reaction } } }
2. Use useEffect to cleanup
javascriptimport { useEffect } from 'react'; import { reaction } from 'mobx'; function TodoList({ store }) { useEffect(() => { const disposer = reaction( () => store.todos, (todos) => { console.log('Todos changed:', todos); } ); return () => disposer(); // Cleanup reaction }, [store]); return <div>{/* ... */}</div>; }
Reaction vs Computed
| Feature | Reaction | Computed |
|---|---|---|
| Purpose | Execute side effects | Calculate derived values |
| Return value | No return value | Returns calculation result |
| Caching | No caching | Automatic caching |
| Trigger timing | Immediately when dependencies change | Only calculated when accessed |
| Use cases | Logging, API calls, DOM operations | Data transformation, filtering, aggregation |
Best Practices
1. Choose reaction type appropriately
javascript// ✅ Use autorun: need immediate execution autorun(() => { console.log('Current state:', this.state); }); // ✅ Use reaction: need to control execution timing reaction( () => this.userId, (id) => this.loadUser(id) ); // ✅ Use when: one-time operation when( () => this.ready, () => this.start() );
2. Avoid modifying dependent state in reaction
javascript// ❌ Wrong: modifying dependent state in reaction reaction( () => this.count, (count) => { this.count = count + 1; // Will cause infinite loop } ); // ✅ Correct: use action to modify other state reaction( () => this.count, (count) => { this.setCount(count + 1); } );
3. Use delay for debouncing
javascript// ✅ Use delay to debounce, avoid frequent triggers reaction( () => this.searchQuery, (query) => this.performSearch(query), { delay: 300 } );
4. Remember to cleanup reaction
javascript// ✅ Cleanup reaction when component unmounts useEffect(() => { const disposer = reaction( () => store.data, (data) => console.log(data) ); return () => disposer(); }, [store]);
Common Mistakes
1. Forgetting to cleanup reaction
javascript// ❌ Wrong: forgetting to cleanup reaction class Component { componentDidMount() { reaction(() => this.store.data, (data) => { console.log(data); }); } } // ✅ Correct: cleanup reaction class Component { disposer = null; componentDidMount() { this.disposer = reaction(() => this.store.data, (data) => { console.log(data); }); } componentWillUnmount() { if (this.disposer) { this.disposer(); } } }
2. Directly modifying state in reaction
javascript// ❌ Wrong: directly modifying state in reaction reaction( () => this.count, (count) => { this.count = count + 1; // Not in action } ); // ✅ Correct: modify state in action reaction( () => this.count, (count) => { runInAction(() => { this.count = count + 1; }); } );
Summary
- reaction is a mechanism in MobX for handling side effects
- autorun is suitable for scenarios requiring immediate execution
- reaction provides finer-grained control
- when is suitable for one-time operations
- Use delay to achieve debounce effect
- Remember to cleanup reaction when component unmounts
- Avoid modifying dependent state in reaction
- Use action or runInAction to modify state