# Integrating MobX with React Context Provider
This section provides a detailed guide on integrating MobX with React's Context API to manage and inject stores across a React application.
Below, we outline best practices, provide a complete example, explain why the RootStore is passed to individual stores, and highlight common pitfalls to avoid with code examples for each.
## Why Use Context with MobX?
* Ensures stores are instantiated once and shared across the application, preventing state duplication or loss on component re-renders.
* Context allows mocking stores in tests, simplifying unit and integration testing.
* Facilitates managing multiple stores in larger applications by providing a centralized access point.
* Aligns with SOLID design principles, particularly single responsibility and dependency inversion.
## Example Implementation
Below is a complete example demonstrating how to set up MobX with a Context Provider in a TypeScript React application, including cross-store interaction.
### Root Store Setup
Create a `RootStore` to hold all domain-specific stores.
```typescript
// stores/RootStore.ts
import { makeAutoObservable } from 'mobx';
import { TodoStore } from './TodoStore';
import { UserStore } from './UserStore';
export class RootStore {
todoStore: TodoStore;
userStore: UserStore;
constructor() {
this.todoStore = new TodoStore(this);
this.userStore = new UserStore(this);
makeAutoObservable(this, {}, { autoBind: true });
}
}
```
```typescript
// stores/TodoStore.ts
export interface Todo {
id: string;
title: string;
done: boolean;
}
export class TodoStore {
todos: Todo[] = [];
loading = false;
error: string | null = null;
rootStore: RootStore;
constructor(rootStore: RootStore) {
makeAutoObservable(this);
this.rootStore = rootStore;
}
addTodo(title: string) {
this.todos.push({ id: Date.now().toString(), title, done: false });
}
*loadTodos() {
if (!this.rootStore.userStore.user?.isLoggedIn) {
this.error = 'User must be logged in to load todos';
return;
}
this.loading = true;
try {
const response = yield fetch('/api/todos');
const data = yield response.json();
this.todos = data;
this.loading = false;
} catch (err) {
this.error = err.message;
this.loading = false;
}
}
}
```
```typescript
// stores/UserStore.ts
export class UserStore {
user: { name: string; isLoggedIn: boolean } | null = null;
rootStore: RootStore;
constructor(rootStore: RootStore) {
makeAutoObservable(this);
this.rootStore = rootStore;
}
setUser(name: string) {
this.user = { name, isLoggedIn: true };
}
}
```
> Passing the `RootStore` to individual stores like `UserStore` and `TodoStore` enables store composition and cross-store communication, ensuring modularity and centralized state management.
### Context and Hook Setup
Create a Context and a custom hook to provide access to the stores.
```typescript
// stores/StoresContext.tsx
const StoresContext = React.createContext<RootStore | null>(null);
export const StoresProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const rootStore = new RootStore();
return <StoresContext.Provider value={rootStore}>{children}</StoresContext.Provider>;
};
export const useStores = (): RootStore => {
const context = React.useContext(StoresContext);
if (!context) {
throw new Error('useStores must be used within a StoresProvider');
}
return context;
};
```
### Using Stores in Components
Wrap your application with the `StoresProvider` and use the `useStores` hook in components.
```typescript
// App.tsx
const App: React.FC = () => {
return (
<StoresProvider>
<TodoList />
</StoresProvider>
);
};
export default App;
```
```typescript
// components/TodoList.tsx
const TodoList = observer(() => {
const { todoStore, userStore } = useStores();
React.useEffect(() => {
if (userStore.user?.isLoggedIn) {
todoStore.loadTodos();
}
}, [todoStore, userStore]);
if (todoStore.loading) return <div>Loading...</div>;
if (todoStore.error) return <div>Error: {todoStore.error}</div>;
return (
<div>
<h1>Todos for {userStore.user?.name || 'Guest'}</h1>
<button onClick={() => todoStore.addTodo(`Task ${todoStore.todos.length + 1}`)}>
Add Todo
</button>
<ul>
{todoStore.todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
});
export default TodoList;
```
### Testing with Mock Stores
Use a mock `RootStore` to test components in isolation.
```typescript
// components/TodoList.test.tsx
describe('TodoList', () => {
it('displays todos', () => {
const mockStore = new RootStore();
mockStore.todoStore.todos = [{ id: '1', title: 'Test Todo', done: false }];
mockStore.userStore.setUser('Test User');
render(
<StoresProvider value={mockStore}>
<TodoList />
</StoresProvider>
);
expect(screen.getByText('Todos for Test User')).toBeInTheDocument();
expect(screen.getByText('Test Todo')).toBeInTheDocument();
});
});
```
## Common Pitfalls and Solutions
Below are common pitfalls when integrating MobX with React Context, along with code examples illustrating the problem and solution for each.
### Missing StoresProvider:
**Problem:** Components throw errors if `useStores` is called outside a `StoresProvider`, causing runtime crashes.
**Example (Problem):**
```typescript
const WrongTodoList = observer(() => {
const { todoStore } = useStores(); // Error: useStores must be used within a StoresProvider
return <div>{todoStore.todos.length} todos</div>;
});
// App.tsx without StoresProvider
const App: React.FC = () => {
return <WrongTodoList />;
};
```
**Solution:** Always wrap your app or relevant subtree with `StoresProvider`.
**Example (Solution):**
```typescript
// App.tsx
const App: React.FC = () => {
return (
<StoresProvider>
<TodoList />
</StoresProvider>
);
};
```
### Multiple Store Instances:
**Problem:** Instantiating stores inside components creates new instances per render, leading to state loss and inconsistent behavior.
**Example (Problem):**
```typescript
// components/WrongTodoList.tsx
const WrongTodoList = observer(() => {
const todoStore = new TodoStore(new RootStore()); // New instance on every render
return (
<div>
<button onClick={() => todoStore.addTodo('Task')}>Add</button>
<ul>{todoStore.todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
</div>
);
});
// Problem: Each render creates a new TodoStore, losing previous state
```
**Solution:** Use Context to provide a single store instance.
**Example (Solution):**
```typescript
// components/TodoList.tsx
const TodoList = observer(() => {
const { todoStore } = useStores(); // Single instance from Context
return (
<div>
<button onClick={() => todoStore.addTodo('Task')}>Add</button>
<ul>{todoStore.todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
</div>
);
});
```
### Unnecessary Re-renders:
**Problem:** Large Context updates can cause unrelated components to re-render, impacting performance.
**Example (Problem):**
```typescript
// components/WrongProfile.tsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { useStores } from '../stores/StoresContext';
const WrongProfile = observer(() => {
const { userStore, todoStore } = useStores(); // Reads all stores
console.log('Profile re-rendered');
return <div>{userStore.user?.name}</div>; // Only needs userStore
});
// Problem: Re-renders when todoStore changes, even though not used
```
**Solution:** Split Contexts for different store groups or use `React.memo` with `observer` for fine-grained updates.
**Example (Solution):**
```typescript
// stores/UserContext.tsx
const UserContext = React.createContext<UserStore | null>(null);
export const UserProvider: React.FC<{ store: UserStore; children: React.ReactNode }> = ({ store, children }) => {
return <UserContext.Provider value={store}>{children}</UserContext.Provider>;
};
export const useUserStore = () => {
const context = React.useContext(UserContext);
if (!context) throw new Error('useUserStore must be used within a UserProvider');
return context;
};
// components/Profile.tsx
const Profile = observer(() => {
const userStore = useUserStore(); // Only subscribes to userStore
console.log('Profile re-rendered');
return <div>{userStore.user?.name}</div>;
});
export default React.memo(Profile); // Prevents re-renders from parent props
```
### Memory Leaks from Reactions:
**Problem:** Failing to dispose of `autorun` or `reaction` can lead to memory leaks, especially in long-lived components.
**Example (Problem):**
```typescript
// components/WrongAuthMonitor.tsx
const WrongAuthMonitor = observer(() => {
const { userStore } = useStores();
reaction(
() => userStore.user?.isLoggedIn,
(isLoggedIn) => {
if (isLoggedIn) console.log('User logged in');
}
); // Never disposed, leaks if component unmounts
return <div>{userStore.user?.name}</div>;
});
```
**Solution:** Return disposer functions in `useEffect` hooks.
**Example (Solution):**
```typescript
// components/AuthMonitor.tsx
const AuthMonitor = observer(() => {
const { userStore } = useStores();
useEffect(() => {
const disposer = reaction(
() => userStore.user?.isLoggedIn,
(isLoggedIn) => {
if (isLoggedIn) console.log('User logged in');
}
);
return () => disposer(); // Clean up on unmount
}, [userStore]);
return <div>{userStore.user?.name}</div>;
});
```
### Overfetching in Components:
**Problem:** Components calling async methods directly can lead to redundant API calls, causing performance issues.
**Example (Problem):**
```typescript
// components/WrongTodoList.tsx
const WrongTodoList = observer(() => {
const { todoStore } = useStores();
const [loading, setLoading] = React.useState(false);
const fetchTodos = async () => {
setLoading(true);
const response = await fetch('/api/todos');
const data = await response.json();
todoStore.todos = data; // Violates MobX action rules
setLoading(false);
};
React.useEffect(() => {
fetchTodos(); // Redundant calls if multiple components do this
}, []);
return <div>{loading ? 'Loading...' : todoStore.todos.length} todos</div>;
});
```
**Solution:** Centralize async logic in stores using `flow` or `runInAction`.
**Example (Solution):**
```typescript
// components/TodoList.tsx
const TodoList = observer(() => {
const { todoStore, userStore } = useStores();
React.useEffect(() => {
if (userStore.user?.isLoggedIn) {
todoStore.loadTodos(); // Centralized async logic
}
}, [todoStore, userStore]);
return <div>{todoStore.loading ? 'Loading...' : todoStore.todos.length} todos</div>;
});
```