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
65 changes: 52 additions & 13 deletions ai-docs/patterns/react-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ an `ErrorBoundary`; a `helper.ts` hook holds business logic and SDK calls; the p
(in `cc-components`) is pure UI driven by props.

**Correct**

```typescript
// from packages/contact-center/user-state/src/user-state/index.tsx
const UserStateInternal: React.FunctionComponent<IUserStateProps> = observer(({onStateChange}) => {
Expand All @@ -36,28 +37,34 @@ const UserStateInternal: React.FunctionComponent<IUserStateProps> = observer(({o
return <UserStateComponent {...props} />;
});
```

The three real layers for this feature:

- Widget: `packages/contact-center/user-state/src/user-state/index.tsx`
- Hook: `packages/contact-center/user-state/src/helper.ts` (`useUserState`)
- Component: `packages/contact-center/cc-components/src/components/UserState/user-state.tsx` (`UserStateComponent`)

**Incorrect**

```typescript
// a presentational component in cc-components reaching into the store
import store from '@webex/cc-store';
export const UserStateComponent = () => {
const {idleCodes} = store; // component must not read the store or call the SDK
};
```

**Why wrong:** It reverses the dependency arrow (`cc-components` must not import the store/SDK) and makes
the component untestable in isolation — it can no longer be driven purely by props. See ADR-0001.

**Where it appears**

- `user-state`: `.../user-state/src/user-state/index.tsx` → `.../user-state/src/helper.ts` → `.../cc-components/src/components/UserState/user-state.tsx`
- `station-login`: `.../station-login/src/station-login/index.tsx` → `.../station-login/src/helper.ts` → `.../cc-components/src/components/StationLogin/station-login.tsx`
- `task` (CallControl): `.../task/src/CallControl/index.tsx` → `.../task/src/helper.ts` → `.../cc-components/src/components/task/CallControl/call-control.tsx`

**Edge cases / exceptions**

- The `task` package has several widgets sharing one `helper.ts` (see the hooks pattern below).
- Small presentational sub-components may compose without their own hook, but data still arrives via props.

Expand All @@ -69,6 +76,7 @@ the component untestable in isolation — it can no longer be driven purely by p
catches render errors and reports them through `store.onErrorCallback`.

**Correct**

```typescript
// from packages/contact-center/task/src/CallControl/index.tsx
const CallControl: React.FunctionComponent<CallControlProps> = (props) => {
Expand All @@ -86,17 +94,21 @@ const CallControl: React.FunctionComponent<CallControlProps> = (props) => {
```

**Incorrect**

```typescript
// exporting the observer component directly, with no boundary
export {CallControlInternal as CallControl};
```

**Why wrong:** A render error in one widget would otherwise bubble up and blank out the whole host page.
The boundary contains the failure to that widget and forwards it to the host via `onErrorCallback`.

**Where it appears**

- `packages/contact-center/user-state/src/user-state/index.tsx` , `packages/contact-center/station-login/src/station-login/index.tsx` , `packages/contact-center/task/src/CallControl/index.tsx` (also `IncomingTask`, `OutdialCall`, `CallControlCAD`)

**Edge cases / exceptions**

- `fallbackRender={() => <></>}` renders nothing on failure by design (widgets are embedded in a host app that owns the surrounding UI).
- The first `onError` argument is the widget name string — keep it matching the widget so host telemetry attributes errors correctly.

Expand All @@ -108,6 +120,7 @@ The boundary contains the failure to that widget and forwards it to the host via
`use*` hook exported from the feature's `helper.ts`, not inline in the widget.

**Correct**

```typescript
// from packages/contact-center/task/src/helper.ts
const loadBuddyAgents = useCallback(async () => {
Expand All @@ -126,24 +139,29 @@ const loadBuddyAgents = useCallback(async () => {
}
}, [logger]);
```

Real hooks: `useUserState` (`user-state/src/helper.ts`), `useStationLogin` (`station-login/src/helper.ts`),
and `useTaskList` / `useIncomingTask` / `useCallControl` / `useOutdialCall` / `useRealTimeTranscript`
(all in `task/src/helper.ts`).

**Incorrect**

```typescript
// SDK call inline in the widget instead of a hook
const CallControlInternal = observer((props) => {
const onHold = () => store.cc.hold(); // logic leaks into the widget
});
```

**Why wrong:** Inline logic can't be unit-tested with `renderHook`, gets duplicated across widgets, and
mixes rendering with side effects. Hooks keep the widget thin and the logic reusable/testable.

**Where it appears**

- `packages/contact-center/user-state/src/helper.ts` , `packages/contact-center/station-login/src/helper.ts` , `packages/contact-center/task/src/helper.ts` (also `packages/contact-center/cc-digital-channels/src/helper.ts`)

**Edge cases / exceptions**

- One `helper.ts` may export several hooks when a package hosts several widgets (the `task` package does).
- A few narrowly-reusable hooks live outside `helper.ts` — e.g. `task/src/Utils/useHoldTimer.ts`, `cc-components/src/hooks/useIntersectionObserver.ts` — when they're shared UI utilities rather than a widget's business logic.

Expand All @@ -155,6 +173,7 @@ mixes rendering with side effects. Hooks keep the widget thin and the logic reus
store or SDK.

**Correct**

```typescript
// from packages/contact-center/cc-components/src/components/UserState/user-state.tsx
const UserStateComponent: React.FunctionComponent<UserStateComponentsProps> = (props) => {
Expand All @@ -169,16 +188,20 @@ const UserStateComponent: React.FunctionComponent<UserStateComponentsProps> = (p
```

**Incorrect**

```typescript
import store from '@webex/cc-store'; // component pulling state itself
```

**Why wrong:** Same as the layering rule — importing the store into `cc-components` reverses the
dependency arrow and destroys prop-driven testability.

**Where it appears**

- `packages/contact-center/cc-components/src/components/UserState/user-state.tsx` , `packages/contact-center/cc-components/src/components/StationLogin/station-login.tsx` , `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` (also `task/IncomingTask`, `task/TaskList`)

**Edge cases / exceptions**

- Components may hold local view-only state (open/closed, hover) and use UI utility hooks; they just never own domain state or call the SDK.

---
Expand All @@ -189,40 +212,47 @@ dependency arrow and destroys prop-driven testability.
cleanup that unregisters the exact same handler.

**Correct**

```typescript
// from packages/contact-center/task/src/helper.ts
useEffect(() => {
if (!currentTask?.data?.interactionId) return;
const interactionId = currentTask.data.interactionId;
if (!currentTask) return;

store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, interactionId);
store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, interactionId);
store.setTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, interactionId);
store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, currentTask);
store.setTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, currentTask);
store.setTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, currentTask);

return () => {
store.removeTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, interactionId);
store.removeTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, interactionId);
store.removeTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, interactionId);
store.removeTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, currentTask);
store.removeTaskCallback(TASK_EVENTS.TASK_RESUME, resumeCallback, currentTask);
store.removeTaskCallback(TASK_EVENTS.TASK_END, endCallCallback, currentTask);
};
}, [currentTask]);
```

Note the repo registers task-scoped listeners through the store's `setTaskCallback` /
`removeTaskCallback` helpers (not raw `cc.on` / `cc.off` in the widget).
`removeTaskCallback` helpers (not raw `cc.on` / `cc.off` in the widget). Pass the `ITask` object
itself (not its `interactionId`) as the third argument — passing an ID requires a `store.taskList`
lookup that can be stale during React 18 StrictMode mount/unmount cycles.

**Incorrect**

```typescript
useEffect(() => {
store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, interactionId);
// no return — handler never removed
store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, currentTask.data.interactionId);
// no return — handler never removed, and passing an ID risks a stale taskList lookup
}, [currentTask]);
```

**Why wrong:** Without cleanup, handlers accumulate across re-renders/task changes, firing multiple times
and holding references to stale task state (a memory + double-fire leak).

**Where it appears**

- `packages/contact-center/task/src/helper.ts` (task callbacks) , `packages/contact-center/user-state/src/helper.ts` (worker lifecycle) , `packages/contact-center/cc-digital-channels/src/helper.ts`

**Edge cases / exceptions**

- The cleanup must reference the **same function identity** passed on registration (define handlers in the hook body or memoize them), or removal is a no-op.

---
Expand All @@ -233,6 +263,7 @@ and holding references to stale task state (a memory + double-fire leak).
passed to a memoized child. Keeps identity stable across renders.

**Correct**

```typescript
// from packages/contact-center/task/src/helper.ts
const getEntryPoints = useCallback(async () => {
Expand All @@ -241,17 +272,25 @@ const getEntryPoints = useCallback(async () => {
```

**Incorrect**

```typescript
const getEntryPoints = async () => { /* ... */ }; // new identity every render
useEffect(() => { getEntryPoints(); }, [getEntryPoints]); // effect re-runs every render
const getEntryPoints = async () => {
/* ... */
}; // new identity every render
useEffect(() => {
getEntryPoints();
}, [getEntryPoints]); // effect re-runs every render
```

**Why wrong:** A fresh function each render changes the effect's dependency identity, re-running the
effect on every render — an infinite-ish fetch loop.

**Where it appears**

- `packages/contact-center/task/src/helper.ts` (`loadBuddyAgents`, `getAddressBookEntries`, `getEntryPoints`, `getQueuesFetcher`, `extractConsultingAgent`).

**Edge cases / exceptions**

- Skip `useCallback` for handlers used only inline in JSX with no memoized child and no effect dependency — the memo overhead buys nothing there.

---
Expand Down
Loading