Here are several common mistakes when working with MobX collections and their React components, along with solutions:
1. Modifying Collections Outside of Actions
Wrong:
typescriptclass TodoStore {todos = []; constructor() { makeAutoObservable(this); } } // In a component function handleDelete(id) { // Direct mutation outside an action const index = todoStore.todos.findIndex(todo => todo.id === id); todoStore.todos.splice(index, 1); // ❌ Modifying outside an action }
Fixed:
typescriptclass TodoStore { todos = []; constructor() { makeAutoObservable(this); } removeTodo(id) { const index = this.todos.findIndex(todo => todo.id === id); if (index !== -1) { this.todos.splice(index, 1); } } } // In a component function handleDelete(id) { todoStore.removeTodo(id); // ✅ Using an action }
2. Creating New References in Render
Wrong:
typescriptconst TodoList = observer(() => { // Creates a new array every render const activeTodos = todoStore.todos.filter(todo => !todo.completed); // ❌ return ( <div> {activeTodos.map(todo => ( <TodoItem key={todo.id} todo={todo} /> ))} </div> ); });
Fixed:
typescriptclass TodoStore { todos = []; constructor() { makeAutoObservable(this); } // Computed property get activeTodos() { return this.todos.filter(todo => !todo.completed); } } const TodoList = observer(() => { return ( <div> {todoStore.activeTodos.map(todo => ( // ✅ Using computed <TodoItem key={todo.id} todo={todo} /> ))} </div> ); });
3. Forgetting to Use Observer
Wrong:
typescript// Not using observer const TodoItem = ({ todo }) => { // ❌ return ( <div> <input type="checkbox" checked={todo.completed} onChange={() => todo.toggle()} /> {todo.title} </div> ); };
Fixed:
typescript// With observer const TodoItem = observer(({ todo }) => { // ✅ return ( <div> <input type="checkbox" checked={todo.completed} onChange={() => todo.toggle()} /> {todo.title} </div> ); });
4. Breaking Observable Chain with Primitive Props
Wrong:
typescriptconst TodoList = observer(() => { return ( <div> {todoStore.todos.map(todo => ( <TodoItem key={todo.id} id={todo.id} // ❌ Passing primitives title={todo.title} // ❌ instead of the object completed={todo.completed} onToggle={() => todo.toggle()} /> ))} </div> ); }); // Component not connected to observable const TodoItem = observer(({ id, title, completed, onToggle }) => { return ( <div> <input type="checkbox" checked={completed} onChange={onToggle} /> {title} </div> ); });
Fixed:
typescriptconst TodoList = observer(() => { return ( <div> {todoStore.todos.map(todo => ( <TodoItem key={todo.id} todo={todo} /> // ✅ Passing the object ))} </div> ); }); const TodoItem = observer(({ todo }) => { return ( <div> <input type="checkbox" checked={todo.completed} onChange={() => todo.toggle()} /> {todo.title} </div> ); });
5. Manual Array Updates Instead of Built-in Methods
Wrong:
typescriptclass TodoStore { todos = []; constructor() { makeAutoObservable(this); } updateTodoTitle(id, newTitle) { // Creating a new array manually this.todos = this.todos.map(todo => { // ❌ Unnecessary recreation if (todo.id === id) { return { ...todo, title: newTitle }; // Creates new object } return todo; }); } }
Fixed:
typescriptclass TodoStore { todos = []; constructor() { makeAutoObservable(this); } updateTodoTitle(id, newTitle) { const todo = this.todos.find(t => t.id === id); if (todo) { todo.title = newTitle; // ✅ Direct mutation within action } } }
6. Over-observing Components
Wrong:
typescript// Parent component observing everything const TodoApp = observer(() => { // ❌ Observing entire store return ( <div> <h1>Todo List ({todoStore.todos.length})</h1> <TodoForm /> <TodoList /> <TodoStats /> </div> ); });
Fixed:
typescript// Parent component doesn't need to observe const TodoApp = () => { // ✅ No observer here return ( <div> <TodoHeader /> {/* This component can observe the count */} <TodoForm /> <TodoList /> <TodoStats /> </div> ); }; // Only observe what's needed const TodoHeader = observer(() => { return <h1>Todo List ({todoStore.todos.length})</h1>; });
7. Unnecessary recomputation with Array.map inside render
Wrong:
typescriptconst TodoList = observer(() => { // This creates a new array of objects on every render const todoItems = todoStore.todos.map(todo => ({ // ❌ id: todo.id, text: `${todo.title} - ${todo.completed ? 'Done' : 'Pending'}` })); return ( <div> {todoItems.map(item => ( <div key={item.id}>{item.text}</div> ))} </div> ); });
Fixed:
typescript// Add a computed property to the Todo class class Todo { id = nanoid(); title = ""; completed = false; constructor(title) { this.title = title; makeAutoObservable(this); } // Computed property handles derived data get displayText() { return `${this.title} - ${this.completed ? 'Done' : 'Pending'}`; } toggle() { this.completed = !this.completed; } } // Component just uses the computed property const TodoList = observer(() => { return ( <div> {todoStore.todos.map(todo => ( <div key={todo.id}>{todo.displayText}</div> // ✅ Using computed property ))} </div> ); });
8. Using makeObservable incorrectly
Wrong:
typescriptclass TodoStore { todos = []; constructor() { // Using makeAutoObservable but still specifying attributes makeAutoObservable(this, { // ❌ Mixing approaches todos: observable, addTodo: action }); } addTodo(title) { this.todos.push(new Todo(title)); } }
Fixed:
typescriptclass TodoStore { todos = []; constructor() { // Either use makeAutoObservable with no attributes makeAutoObservable(this); // ✅ Correct usage // OR use makeObservable with explicit attributes /* makeObservable(this, { todos: observable, addTodo: action, activeTodos: computed }); */ } addTodo(title) { this.todos.push(new Todo(title)); } get activeTodos() { return this.todos.filter(todo => !todo.completed); } }
These examples illustrate the most common pitfalls and their solutions. The key principles are: use actions for mutations, leverage computed properties for derived data, pass references instead of primitives, and use the observer HOC judiciously.