-
Notifications
You must be signed in to change notification settings - Fork 62
docs(ai-docs): add patterns documentation - TypeScript, React, MobX, Web Components, Testing #591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
| ``` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused computed pattern Issue: Suggestion:
Computed Pattern |
||
|
|
||
| --- | ||
|
|
||
| ## 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) | ||
There was a problem hiding this comment.
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
});
}
}