In MobX, action is the only recommended way to modify observable state. Using action ensures that state changes are traceable, predictable, and can optimize performance.
Purpose of Action
- Batch updates: All state changes within an action are batched, reducing unnecessary recalculations
- Traceability: All state changes are concentrated in actions, making debugging and tracking easier
- Transactional: State changes within an action are atomic, either all succeed or all fail
- Performance optimization: Reduce the number of reaction triggers, improving performance
Action Usage Methods
1. Using Decorators
javascriptimport { observable, action } from 'mobx'; class TodoStore { @observable todos = []; @action addTodo(text) { this.todos.push({ text, completed: false }); } @action.bound removeTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id); } @action async fetchTodos() { const response = await fetch('/api/todos'); const data = await response.json(); this.todos = data; } }
2. Using makeObservable
javascriptimport { makeObservable, observable, action } from 'mobx'; class TodoStore { todos = []; constructor() { makeObservable(this, { todos: observable, addTodo: action, removeTodo: action.bound, fetchTodos: action }); } addTodo(text) { this.todos.push({ text, completed: false }); } removeTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id); } async fetchTodos() { const response = await fetch('/api/todos'); const data = await response.json(); this.todos = data; } }
3. Using runInAction
javascriptimport { observable, runInAction } from 'mobx'; class TodoStore { todos = []; async fetchTodos() { const response = await fetch('/api/todos'); const data = await response.json(); runInAction(() => { this.todos = data; }); } }
Action Types
1. action
The most basic action decorator, used to wrap state modification methods.
javascript@action updateTodo(id, updates) { const todo = this.todos.find(t => t.id === id); if (todo) { Object.assign(todo, updates); } }
2. action.bound
Action that automatically binds this, avoiding losing this context in callback functions.
javascript@action.bound handleClick() { this.count++; } // No need to bind when using <button onClick={this.handleClick}>Click</button>
3. runInAction
Used when modifying state in asynchronous operations.
javascriptasync loadData() { const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; this.loading = false; }); }
Action Best Practices
1. Always modify state in actions
javascript// ❌ Wrong: directly modifying state class Store { @observable count = 0; increment() { this.count++; // Not an action } } // ✅ Correct: modifying state in action class Store { @observable count = 0; @action increment() { this.count++; } }
2. Use action.bound for event handlers
javascriptclass Component { @observable count = 0; @action.bound handleClick() { this.count++; } render() { return <button onClick={this.handleClick}>Click</button>; } }
3. Use runInAction in asynchronous operations
javascript@action async fetchUser(id) { this.loading = true; try { const response = await fetch(`/api/users/${id}`); const data = await response.json(); runInAction(() => { this.user = data; this.loading = false; }); } catch (error) { runInAction(() => { this.error = error.message; this.loading = false; }); } }
4. Reasonably split actions
javascript// ❌ Too complex action @action handleComplexOperation(data) { this.loading = true; this.data = data; this.filtered = data.filter(item => item.active); this.count = this.filtered.length; this.timestamp = Date.now(); this.loading = false; } // ✅ Split into multiple small actions @action handleComplexOperation(data) { this.setLoading(true); this.setData(data); this.processData(data); this.setLoading(false); } @action setLoading(loading) { this.loading = loading; } @action setData(data) { this.data = data; } @action processData(data) { this.filtered = data.filter(item => item.active); this.count = this.filtered.length; this.timestamp = Date.now(); }
Common Mistakes
1. Directly modifying state in async callbacks
javascript// ❌ Wrong @action async fetchData() { const data = await fetch('/api/data'); this.data = await data.json(); // Not in action } // ✅ Correct @action async fetchData() { const data = await fetch('/api/data'); runInAction(() => { this.data = data.json(); }); }
2. Forgetting to use action.bound
javascript// ❌ Wrong: this might be lost class Component { @action handleClick() { this.count++; } render() { return <button onClick={this.handleClick}>Click</button>; } } // ✅ Correct: use action.bound class Component { @action.bound handleClick() { this.count++; } render() { return <button onClick={this.handleClick}>Click</button>; } }
Performance Optimization
- Use action for batch updates: Reduce reaction trigger count
- Avoid creating actions in loops: Reuse action functions
- Use runInAction reasonably: Only use when necessary
- Use action.bound: Avoid repeated binding
Summary
- action is the only recommended way to modify observable state
- Using action can batch updates, improve performance, and facilitate debugging
- Prioritize using action.bound for event handlers
- Use runInAction to modify state in asynchronous operations
- Reasonably split complex actions to improve maintainability