---
# System prepended metadata

title: Integrating MobX with React Context Provider

---

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