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

What are the types and use cases of reaction in MobX?

2月22日 14:05

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.

javascript
import { 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.

javascript
import { 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.

javascript
import { 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.

javascript
reaction( () => this.filter, (filter) => { console.log('Current filter:', filter); }, { fireImmediately: true } // Execute immediately once );

2. delay

Delay execution, debounce effect.

javascript
reaction( () => this.searchQuery, (query) => { this.performSearch(query); }, { delay: 300 } // Delay execution by 300ms );

3. equals

Custom comparison function to determine whether to trigger reaction.

javascript
reaction( () => 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.

javascript
reaction( () => this.todos, (todos) => { console.log('Todos updated'); }, { name: 'saveTodosToLocalStorage' } );

Reaction Use Cases

1. Data Persistence

javascript
class 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

javascript
class 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

javascript
class 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; }); } } } ); } }
javascript
class 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

javascript
class 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

javascript
class 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

javascript
import { 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

FeatureReactionComputed
PurposeExecute side effectsCalculate derived values
Return valueNo return valueReturns calculation result
CachingNo cachingAutomatic caching
Trigger timingImmediately when dependencies changeOnly calculated when accessed
Use casesLogging, API calls, DOM operationsData 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
标签:Mobx