Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 308 additions & 0 deletions ai-docs/patterns/mobx-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
# MobX Patterns

> Quick reference for LLMs working with MobX state management in this repository.

---

## Rules

- **MUST** use the singleton store pattern via `Store.getInstance()`
- **MUST** wrap widgets with `observer` HOC from `mobx-react-lite`
- **MUST** use `runInAction` for all state mutations
- **MUST** access store via `import store from '@webex/cc-store'`
- **MUST** mark state properties as `observable`
- **MUST** use `makeObservable` in store constructor
- **NEVER** mutate state outside of `runInAction`
- **NEVER** access store directly in presentational components
- **NEVER** create multiple store instances

---

## Store Singleton Pattern

```typescript
// store.ts
import { makeObservable, observable, action, runInAction } from 'mobx';

class Store {
private static instance: Store;

// Observable state
@observable agentId: string = '';
@observable currentState: string = '';
@observable idleCodes: IdleCode[] = [];
@observable isLoggedIn: boolean = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect MobX decorator pattern

Issue: Documentation shows decorator-based MobX patterns (@observable, @action, @computed), but the actual codebase uses makeAutoObservable() without decorators.

Current (WRONG):

import { observable, makeObservable } from 'mobx';

class Store {
@observable agentId: string = '';
@observable teams: Team[] = [];

constructor() {
makeObservable(this);
}
}

Actual Implementation (packages/contact-center/store/src/store.ts:54-58):

class Store implements IStore {
teams: Team[] = [];
agentId: string = '';
// NO DECORATORS!

constructor() {
makeAutoObservable(this, {
cc: observable.ref,
});
}
}

Required Fix:
Replace the entire "Observable Decorator Pattern" section with:

makeAutoObservable Pattern

ALWAYS use makeAutoObservable for store classes:

import { makeAutoObservable, observable } from 'mobx';

class Store {
// Plain property declarations (no decorators)
agentId: string = '';
teams: Team[] = [];
currentState: string = '';
isLoggedIn: boolean = false;

constructor() {
// makeAutoObservable automatically makes properties observable
makeAutoObservable(this, {
// Only specify overrides for special cases
cc: observable.ref, // Don't observe nested properties
});
}
}


private constructor() {
makeObservable(this);
}

static getInstance(): Store {
if (!Store.instance) {
Store.instance = new Store();
}
return Store.instance;
}
}

export default Store.getInstance();
```

---

## Observable Decorator Pattern

```typescript
import { observable, makeObservable } from 'mobx';

class Store {
@observable agentId: string = '';
@observable teams: Team[] = [];
@observable currentState: string = 'Available';
@observable tasks: ITask[] = [];

constructor() {
makeObservable(this);
}
}
```

---

## runInAction Pattern

**ALWAYS use runInAction for state mutations:**

```typescript
import { runInAction } from 'mobx';

// ✅ CORRECT
const handleLogin = async () => {
const result = await cc.login();
runInAction(() => {
store.agentId = result.agentId;
store.isLoggedIn = true;
store.teams = result.teams;
});
};

// ❌ WRONG - Direct mutation
const handleLogin = async () => {
const result = await cc.login();
store.agentId = result.agentId; // NOT ALLOWED
};
```

---

## Observer HOC Pattern

**ALWAYS wrap widgets that access store with observer:**

```typescript
import { observer } from 'mobx-react-lite';
import store from '@webex/cc-store';

const UserStateInternal: React.FC<Props> = observer((props) => {
// Access store - component will re-render when these change
const { currentState, idleCodes, agentId } = store;

return (
<UserStateComponent
currentState={currentState}
idleCodes={idleCodes}
/>
);
});
```

---

## Store Import Pattern

```typescript
// ✅ CORRECT - Import singleton
import store from '@webex/cc-store';

const MyWidget = observer(() => {
const { agentId, teams } = store;
// ...
});

// ❌ WRONG - Creating new instance
import { Store } from '@webex/cc-store';
const store = new Store(); // NOT ALLOWED
```

---

## Action Pattern

```typescript
import { action, makeObservable } from 'mobx';

class Store {
@observable currentState: string = '';

constructor() {
makeObservable(this);
}

@action
setCurrentState(state: string) {
this.currentState = state;
}

@action
reset() {
this.currentState = '';
this.agentId = '';
this.isLoggedIn = false;
}
}
```

---

## Computed Pattern

```typescript
import { observable, computed, makeObservable } from 'mobx';

class Store {
@observable tasks: ITask[] = [];

constructor() {
makeObservable(this);
}

@computed
get activeTasks(): ITask[] {
return this.tasks.filter(task => task.status === 'active');
}

@computed
get taskCount(): number {
return this.tasks.length;
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unused computed pattern

Issue:
Documentation shows @computed pattern, but it's not used anywhere in the actual store.

Suggestion:
Either:

  1. Remove this section entirely, OR
  2. Add a note:

Computed Pattern

⚠️ Note: Computed properties are currently NOT used in the cc-store implementation. This pattern is shown for reference if needed in the future.


---

## Event Handling with Store Pattern

```typescript
import { runInAction } from 'mobx';
import store from '@webex/cc-store';

// In helper.ts or hook
useEffect(() => {
const handleTaskIncoming = (task: ITask) => {
runInAction(() => {
store.incomingTask = task;
});
};

store.cc.on(TASK_EVENTS.TASK_INCOMING, handleTaskIncoming);

return () => {
store.cc.off(TASK_EVENTS.TASK_INCOMING, handleTaskIncoming);
};
}, []);
```

---

## Store Wrapper Pattern

```typescript
// storeEventsWrapper.ts
import { runInAction } from 'mobx';
import store from './store';

export const initStoreEventListeners = () => {
store.cc.on(CC_EVENTS.AGENT_STATE_CHANGED, (data) => {
runInAction(() => {
store.currentState = data.state;
store.lastStateChangeTimestamp = Date.now();
});
});

store.cc.on(CC_EVENTS.AGENT_LOGOUT_SUCCESS, () => {
runInAction(() => {
store.reset();
});
});
};
```

---

## Store Access in Widgets

```typescript
// Widget file
import { observer } from 'mobx-react-lite';
import store from '@webex/cc-store';

const StationLoginInternal = observer(() => {
// Destructure what you need from store
const {
cc,
teams,
dialNumbers,
isAgentLoggedIn,
loginConfig,
} = store;

// Use in component
return (
<StationLoginComponent
teams={teams}
dialNumbers={dialNumbers}
isLoggedIn={isAgentLoggedIn}
/>
);
});
```

---

## Async Action Pattern

```typescript
import { runInAction } from 'mobx';

const fetchData = async () => {
// Set loading state
runInAction(() => {
store.isLoading = true;
store.error = null;
});

try {
const result = await store.cc.fetchTeams();

// Update with result
runInAction(() => {
store.teams = result.teams;
store.isLoading = false;
});
} catch (error) {
// Handle error
runInAction(() => {
store.error = error;
store.isLoading = false;
});
}
};
```

---

## Related

- [React Patterns](./react-patterns.md)
- [TypeScript Patterns](./typescript-patterns.md)
- [Testing Patterns](./testing-patterns.md)
Loading