TypeScript Coding Standards
Goal: Write type-safe React components with proper MSAL integration
Hot Module Replacement (HMR) Workflow
The frontend runs with Vite HMR. When you edit TypeScript/React code:
- •Save the file - Vite instantly updates the browser (no refresh needed)
- •Check the terminal - Look for HMR updates in the "Frontend: React Vite" terminal
- •State is preserved - React state persists through most edits
VS Code Tasks (use Run Task command or check terminal panel):
- •
Frontend: React Vite- Runsnpm run devwith HMR enabled - •Logs are visible directly in VS Code terminal
No restart needed - Just edit, save, and see changes instantly in the browser.
Testing changes: Use Playwright browser tools to:
- •Navigate to http://localhost:5173
- •Check browser console logs for state transitions and errors
- •Inspect network requests for API validation
TypeScript Config
Enable strict mode + explicit types (avoid any):
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
React Components
Use functional components + hooks + typed props:
interface MessageProps {
message: string;
sender: 'user' | 'agent';
}
function Message({ message, sender }: MessageProps) {
return <div className={`msg-${sender}`}>{message}</div>;
}
MSAL Pattern
Always: Try silent first, fallback to popup:
try {
const { accessToken } = await instance.acquireTokenSilent({
...tokenRequest,
account: accounts[0]
});
return accessToken;
} catch {
const { accessToken } = await instance.acquireTokenPopup(tokenRequest);
return accessToken;
}
Environment Variables
CRITICAL: Access at module level only (build-time replacement):
// ✅ Correct - module level
const clientId = import.meta.env.VITE_ENTRA_SPA_CLIENT_ID;
// ❌ Wrong - inside function (won't work after build)
function getClientId() {
return import.meta.env.VITE_ENTRA_SPA_CLIENT_ID;
}
Available variables:
- •
VITE_ENTRA_SPA_CLIENT_ID- Entra app client ID - •
VITE_ENTRA_TENANT_ID- Azure tenant ID
State Management
Use useState (local) or Context API (shared):
const [messages, setMessages] = useState<Message[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null);
Memoization Patterns
Use useMemo and useCallback for expensive computations and stable references:
// Memoize computed values
const isAuthenticated = useMemo(
() => accounts.length > 0,
[accounts.length]
);
// Memoize callbacks to prevent child re-renders
const getAccessToken = useCallback(async () => {
// ... token acquisition logic
}, [instance, accounts]);
// Return memoized object for stable reference
return useMemo(
() => ({ getAccessToken, isAuthenticated, user }),
[getAccessToken, isAuthenticated, user]
);
API Calls
Include Authorization header + use async/await:
const response = await fetch('/api/endpoint', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
npm Dependencies
React 19: Use --legacy-peer-deps flag:
npm install --legacy-peer-deps
IMPORTANT: --legacy-peer-deps skips automatic peer dependency installation. If a package requires peer dependencies, you must add them explicitly to package.json.
Example: @fluentui-copilot/react-copilot → @lexical/react → @lexical/yjs requires yjs as a peer dependency. Since peer deps aren't auto-installed, yjs must be in package.json directly.
Before committing package changes, always verify with:
npm ci # Fails if lock file is out of sync with package.json
If npm ci fails with "Missing: <package> from lock file", add the missing package explicitly to package.json and run npm install --legacy-peer-deps again.
Common Mistakes
- •❌ Accessing
import.meta.env.*in functions - •❌ Calling hooks conditionally or in loops
- •❌ Using
anytype - •❌ Storing tokens in component state
- •❌ Running
npm installwithout--legacy-peer-deps - •❌ Missing memoization in custom hooks (causes infinite re-renders)
- •❌ Returning new objects from hooks without
useMemo
Project-Specific: Architecture
| Concern | Implementation |
|---|---|
| State Management | Centralized Context + useReducer (AppContext) with discriminated action union |
| Authentication | MSAL redirect flow; silent token refresh; useAuth hook |
| Chat Streaming | SSE in ChatService with abort controllers for cancellation |
| Accessibility | Live region (aria-live), aria labels, focus management |
| Logging | Dev-only diff-based logger |
Project-Specific: Key Components
| Component | Purpose |
|---|---|
AgentPreview.tsx | Container wiring chat state to controlled ChatInterface |
ChatInterface.tsx | Stateless controlled UI; renders messages, input, errors, BuiltWithBadge |
chat/AssistantMessage.tsx | Memoized assistant message with streaming + citation footnotes |
chat/UserMessage.tsx | Memoized user message with image thumbnail previews |
chat/ChatInput.tsx | File uploads, character counter, cancel streaming button |
chat/CitationMarker.tsx | Inline superscript citation badge with tooltip + click handler |
core/Markdown.tsx | Renders markdown with inline citation markers via ContentWithCitations |
core/BuiltWithBadge.tsx | "Built with Azure AI Foundry" link badge (centered under input) |
Project-Specific: Citation System
Parser: frontend/src/utils/citationParser.ts
Handles Azure AI Agent citation formats:
- •Assistants/Responses API:
【4:0†source】,【13†myfile.pdf】 - •Azure OpenAI On Your Data:
[doc1],[doc2]
Flow:
- •
parseContentWithCitations()replaces placeholders with[N]markers - •
Markdown.tsxrendersCitationMarkercomponents for each[N] - •Clicking inline marker scrolls to footnote (with highlight animation) or opens URL
- •
AssistantMessage.tsxrenders footnote list with icons by type (URI/file/document)
Key Types (frontend/src/types/chat.ts):
- •
IAnnotation- Citation metadata (type, label, url, fileId, quote, textToReplace) - •
IndexedCitation- Parsed citation with display index
Project-Specific: File Upload Validation
Limits: 5MB per file, max 5 files total
See: frontend/src/utils/fileAttachments.ts for validateImageFile() and validateFileCount()
Project-Specific: ChatService
File: frontend/src/services/chatService.ts
Key patterns:
- •Class-based service with
Dispatch<AppAction>for state updates - •
AbortControllerfor stream cancellation (cancelStream()) - •
retryWithBackoff()for resilient API calls (3 retries, 1s initial delay) - •SSE parsing via
parseSseLine()andsplitSseBuffer()utilities - •Duplicate chunk suppression to prevent UI flicker
Methods:
| Method | Purpose |
|---|---|
sendMessage() | Orchestrates auth, file conversion, streaming |
cancelStream() | Aborts active stream, dispatches CHAT_CANCEL_STREAM |
clearChat() | Resets conversation state |
clearError() | Clears error without affecting chat |
Project-Specific: Adding Features
- •Extend state: Add discriminated action to
AppActionunion infrontend/src/types/appState.ts - •Handle in reducer: Update
frontend/src/reducers/appReducer.ts(keep pure, no side effects) - •Create service method: Add to
ChatServiceif network interaction needed - •Wire container: Update
AgentPreview.tsxto dispatch actions - •Update UI: Pass callbacks to controlled component
Project-Specific: Accessibility Checklist
- •✅ Live region announces latest assistant message
- •✅
aria-busyattribute on messages container during streaming - •✅ Buttons have
aria-labelwhen icon-only - •✅ Focus returns to input after sending
- •✅ Character counter linked via
aria-describedby
Related Skills
- •implementing-chat-streaming - SSE streaming patterns and frontend state flow
- •troubleshooting-authentication - MSAL popup issues and token debugging
- •testing-with-playwright - Browser testing and accessibility validation