# 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>; }); ```