Instruction file imported from marcoacciarri/next-convex-ai-saas-boilerplate (
.github/instructions/components.instructions.md). Copyright stays with the author.
Component Architecture Guidelines
This document outlines the component structure and patterns to follow when creating new components in this Next.js + Material-UI + Convex Agent project.
Quick Reference for AI Agents
MANDATORY REQUIREMENTS:
- ✅ Use MUI v5 +
tss-react/muifor ALL styling (see theming.instructions.md) - ✅ Every component needs
index.tsx+index.style.tsfiles - ✅ Named exports only:
export const ComponentName = () => {} - ✅ Add
data-testidattributes on all structural elements - ✅ Path aliases:
@services/*,@components/*,@scenes/*(NOT relative imports) - ✅ Smart/Block separation: logic vs presentation
- ✅ Convex integration: Use proper hooks and API imports
PROHIBITED:
- ❌ Inline styles
style={{}}(see theming.instructions.md) - ❌ Default exports
- ❌ Missing
data-testidattributes - ❌ Relative imports for services/components/scenes
- ❌ Direct Convex client usage (use hooks)
Table of Contents
- Core Principles
- Component Types
- Folder Structure
- Code Patterns
- Convex Integration
- Agent System Integration
- Import/Export Conventions
- Testing & Accessibility
- Examples
- Component Templates
- Best Practices
- Prohibitions
🎨 Styling & Theming: For complete styling guidelines, see theming.instructions.md
Core Principles
📂 Directory Structure: For project layout details, see other documentation files
1. Separation of Concerns
- Smart Components: Handle state management, business logic, API calls
- Blocks (Presentation Components): Pure presentation, receive data via props
- Styling: MANDATORY - Isolated in separate
.style.tsfiles usingtss-react/mui(see theming.instructions.md)
2. Component Composition
- Break complex components into smaller, focused blocks
- Create reusable blocks that can be composed together
- Each block should have a single responsibility
- Use MUI components (
Box,Stack,Typography,Button, etc.) for layout and elements
3. Maintainability & Standards
- Clear file structure and naming conventions (PascalCase for components)
- Predictable import patterns using path aliases
- Consistent component declaration patterns (named exports only)
- Styling standards - See theming.instructions.md for complete styling guidelines
- NO Emotion styled API - Only
tss-react/muiis permitted
4. Convex Integration
- Clear integration with Convex backend functions
- Use proper React hooks for Convex functions
- Handle authentication states correctly
- Integrate with agent framework for chat functionality
Convex Integration
Backend Function Integration
Always use Convex React hooks for backend integration:
// ✅ Correct - Use Convex hooks
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
export const ComponentName = () => {
const threads = useQuery(api.threads.listUserThreads, {});
const createThread = useMutation(api.threads.createChatThread);
const handleCreate = async () => {
const threadId = await createThread({ title: "New Chat" });
};
return <div>...</div>;
};
// ❌ Avoid - Direct client usage
import { ConvexReactClient } from "convex/react";
const client = new ConvexReactClient(...);
Available Convex Functions
Thread Management:
// Import: api.threads.*
api.threads.createChatThread; // Create authenticated thread
api.threads.createAnonymousThread; // Create anonymous thread (homepage)
api.threads.listUserThreads; // List user's threads (dashboard)
api.threads.updateThreadTitle; // Rename thread
api.threads.deleteThread; // Delete thread
api.threads.getThread; // Get thread details
Chat Functions:
// Import: api.chat.*
api.chat.sendMessageWithStreaming; // Send message with agent selection
api.chat.listThreadMessages; // Get thread messages with streaming
api.chat.getAgentSuggestion; // Get AI agent suggestion
Authentication:
// Import: api.authUtils.*
api.authUtils.getCurrentUserId; // Get current user ID
api.authUtils.clearAuthData; // Clear auth data on signout
Document Functions:
// Import: api.documents.*
api.documents.getUserDocuments; // List user's documents
api.documents.generateUploadUrl; // Get file upload URL
api.documents.uploadDocument; // Register uploaded document
api.documents.deleteDocument; // Delete document
api.rag.semanticSearchRAG; // Search documents with RAG
Authentication Integration
Use Convex Auth hooks for authentication state:
import { useConvexAuth, useAuthActions } from "@convex-dev/auth/react";
export const ComponentName = () => {
const { isAuthenticated, isLoading } = useConvexAuth();
const { signIn, signOut } = useAuthActions();
if (isLoading) {
return <CircularProgress />;
}
if (!isAuthenticated) {
return <SignInButton onClick={() => signIn("github")} />;
}
return <AuthenticatedContent />;
};
⚠️ Critical Type Mapping Patterns
IMPORTANT: Convex hook return types don't always match component prop expectations. Always transform the data:
// ❌ WRONG - Direct assignment causes type errors
const messagesResult = useThreadMessages(...);
<Messages messagesResult={messagesResult} /> // Type error!
// ✅ CORRECT - Transform to expected structure
const messagesResult = useThreadMessages(
api.chat.listThreadMessages as ThreadQuery,
currentThreadId ? { threadId: currentThreadId } : "skip",
{ initialNumItems: 50 }
);
// Transform to component-expected format
const transformedMessagesResult = {
messages: messagesResult?.results || [],
loadMore: messagesResult?.loadMore ? () => messagesResult.loadMore(10) : undefined,
isLoadingMore: messagesResult?.isLoading,
hasLoadedAll: false, // Set based on your pagination logic
};
<Messages messagesResult={transformedMessagesResult} />
Parameter Name Validation
CRITICAL: Always verify Convex mutation parameter names match the schema exactly:
// ✅ Always check convex/schema.ts or function definitions first
// Example: uploadDocument expects { fileName, fileSize, mimeType, fileId }
// ❌ WRONG - Parameter names don't match schema
await uploadDocument({
name: file.name, // Schema expects 'fileName'
size: file.size, // Schema expects 'fileSize'
storageId: id, // Schema expects 'fileId'
});
// ✅ CORRECT - Match schema exactly
await uploadDocument({
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
fileId: storageId,
});
Convex ID Type Handling
Handle Convex ID types properly:
// ✅ Import proper types
import { Id } from "@/convex/_generated/dataModel";
// ✅ Use correct interface definitions
interface Document {
_id: Id<"documents">; // Not string!
fileId: Id<"_storage">; // Not string!
// ... other properties
}
// ✅ Type deletion handlers correctly
const handleDelete = async (documentId: Id<"documents">) => {
await deleteDocument({ documentId }); // Convex expects Id<"documents">
};
Agent System Integration
Agent Framework Integration
For chat components, integrate with the Convex Agent Framework:
import {
useThreadMessages,
optimisticallySendMessage,
ThreadQuery,
} from "@convex-dev/agent/react";
export const ChatComponent = ({ threadId }: { threadId: string }) => {
// Use agent framework hooks for messages
const messagesResult = useThreadMessages(
api.chat.listThreadMessages as ThreadQuery,
{ threadId },
{ initialNumItems: 50 },
);
// Use optimistic updates for better UX
const sendMessage = useMutation(
api.chat.sendMessageWithStreaming,
).withOptimisticUpdate(
optimisticallySendMessage(api.chat.listThreadMessages as ThreadQuery),
);
return <div>...</div>;
};
Agent Types and Configuration
Support the multi-agent system:
// Agent type definition
type AgentType = "chat" | "document" | "web";
interface AgentConfig {
name: string;
icon: string;
description: string;
color: "primary" | "secondary" | "success" | "warning" | "info" | "error";
}
const AGENT_CONFIGS: Record<AgentType, AgentConfig> = {
chat: {
name: "Chat Assistant",
icon: "💬",
description: "General conversation and assistance",
color: "primary",
},
document: {
name: "Document Analyzer",
icon: "📄",
description: "Analyze uploaded documents and files",
color: "info",
},
web: {
name: "Web Assistant",
icon: "🌐",
description: "Browse web pages and online content",
color: "success",
},
};
export const AgentSelector = ({
currentAgent,
onAgentChange,
}: {
currentAgent: AgentType;
onAgentChange: (agent: AgentType) => void;
}) => {
// Component implementation...
};
Streaming Message Support
Handle real-time streaming messages:
export const Messages = ({ threadId }: { threadId: string }) => {
const messagesResult = useThreadMessages(
api.chat.listThreadMessages as ThreadQuery,
{ threadId },
{ initialNumItems: 50 },
);
const { messages, loadMore, isLoadingMore, hasLoadedAll } = messagesResult;
// Handle streaming states
const enhancedMessages = messages.map((message) => ({
...message,
// Add agent information for display
agentType: message.agentType as AgentType | undefined,
}));
return (
<Stack spacing={2}>
{enhancedMessages.map((message) => (
<MessageBubble
key={message._id}
message={message}
agentConfig={
message.agentType ? AGENT_CONFIGS[message.agentType] : undefined
}
/>
))}
</Stack>
);
};
Component Types
Smart Components
Purpose: Orchestrate application logic and state Characteristics:
- Manage state with
useState,useEffect, etc. - Handle API calls and business logic
- Pass data down to blocks via props
- Located in main component folders
Blocks (Presentation Components)
Purpose: Handle pure presentation and user interactions Characteristics:
- Receive all data via props
- No direct state management (except local UI state)
- Highly reusable and testable
- Located in
blocks/subfolder
Folder Structure
🎨 For styling requirements and
.style.tsfile conventions, see theming.instructions.md
Component with Blocks
components/
├── ComponentName/
│ ├── index.tsx # Smart component (state management)
│ ├── index.style.ts # Main component styles
│ └── blocks/
│ ├── index.ts # Barrel exports
│ ├── BlockOne/
│ │ ├── index.tsx # Block component
│ │ └── index.style.ts # Block styles
│ └── BlockTwo/
│ ├── index.tsx
│ └── index.style.ts
Code Patterns
Component Declaration
Always use named arrow function exports:
// ✅ Correct
export const ComponentName = () => {
// component logic
};
// ❌ Avoid
export default function ComponentName() {}
const ComponentName = () => {};
export default ComponentName;
Import Order Conventions
Follow this specific import order:
// 1. React and external libraries
import React from "react";
import { useState, useEffect } from "react";
// 2. MUI components
import { Box, Stack, Typography, Button, Divider, Icon } from "@mui/material";
// 3. Local styles and helpers
import { useStyles } from "./index.style";
// 4. Local subcomponents and hooks
import { WelcomeView, ChatView } from "./blocks";
import { useCustomHook } from "@hooks/useCustomHook";
Component Structure Pattern
Use this standard pattern for all components:
import React from "react";
import { Box, Stack, Typography } from "@mui/material";
import { useStyles } from "./index.style";
interface eProps {
title: string;
onAction: () => void;
}
export const ComponentName = (props: Props) => {
const { title, onAction } = props;
const { classes } = useStyles();
return (
<Stack className={classes.root} data-testid="componentName">
<Box className={classes.content} data-testid="componentNameContent">
<Typography variant="h6">{title}</Typography>
</Box>
</Stack>
);
};
Client Components
Add "use client" directive when needed:
// ✅ For components using hooks, state, or browser APIs
"use client";
import { useState } from "react";
// ...
Import/Export Conventions
Path Aliases
Use path aliases for cleaner imports:
// ✅ Correct - Use path aliases
import { ChatMessage } from "@services/geminiClient";
import { Header } from "@components/Header";
// ❌ Avoid - Relative paths
import { ChatMessage } from "../../../../services/geminiClient";
import { Header } from "../../Header";
Barrel Exports
Create barrel exports for blocks:
// blocks/index.ts
export { MessageInput } from "./MessageInput";
export { Message } from "./Message";
export { WelcomeView } from "./WelcomeView";
// Main component
import { WelcomeView, ChatView } from "./blocks";
Material-UI Imports
Group MUI imports efficiently:
// ✅ Correct
import { Box, Typography, Button, TextField } from "@mui/material";
import SendIcon from "@mui/icons-material/Send";
Testing & Accessibility
Data Test IDs
Add mandatory data-testid attributes following naming patterns:
export const ComponentName = () => {
const { classes } = useStyles();
return (
<Stack className={classes.root} data-testid="componentNameRoot">
<Box className={classes.content} data-testid="componentNameContent">
<Typography variant="h6">Title</Typography>
<Button data-testid="componentNameAction">Action</Button>
</Box>
<Divider data-testid="componentNameDivider" />
</Stack>
);
};
Naming Convention for Test IDs
- Root container:
data-testid="componentNameRoot" - Content areas:
data-testid="componentNameContent" - Interactive elements:
data-testid="componentNameAction",data-testid="componentNameButton" - Dividers:
data-testid="componentNameDivider" - Format: Always camelCase with component name prefix
Accessibility Requirements
Add accessibility attributes where appropriate:
// ✅ Correct - Accessibility attributes
<Button
aria-label="Send message"
data-testid="sendButton"
onClick={onSend}
>
<SendIcon />
</Button>
<Box role="main" aria-labelledby="main-heading">
<Typography id="main-heading" variant="h1">
Dashboard
</Typography>
</Box>
Examples
🎨 For styling examples and theme usage, see theming.instructions.md
Smart Component Example
"use client";
import { useState, useEffect } from "react";
import { useStyles } from "./index.style";
import { DataService } from "@services/dataService";
import { ListView, WelcomeView } from "./blocks";
interface DataItem {
id: string;
name: string;
}
export const DataManager = () => {
const { classes } = useStyles();
const [items, setItems] = useState<DataItem[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const data = await DataService.fetchItems();
setItems(data);
} catch (error) {
console.error("Failed to load data:", error);
} finally {
setLoading(false);
}
};
if (items.length === 0 && !loading) {
return <WelcomeView onStart={loadData} />;
}
return <ListView items={items} loading={loading} onRefresh={loadData} />;
};
Block Component Example
"use client";
import React from "react";
import { Box, Typography, Button } from "@mui/material";
import { useStyles } from "./index.style";
interface WelcomeViewProps {
onStart: () => void;
loading?: boolean;
}
export const WelcomeView = (props: WelcomeViewProps) => {
const { onStart, loading } = props;
const { classes } = useStyles();
return (
<Box className={classes.container}>
<Typography variant="h4" className={classes.title}>
Welcome to Data Manager
</Typography>
<Typography variant="body1" className={classes.description}>
Click the button below to get started.
</Typography>
<Button
variant="contained"
onClick={onStart}
disabled={loading}
className={classes.startButton}
>
{loading ? "Loading..." : "Get Started"}
</Button>
</Box>
);
};
Component Templates
Basic Component Template
Copy/paste template for new components:
import React from "react";
import { Box, Stack, Typography } from "@mui/material";
import { useStyles } from "./index.style";
interface ComponentNameProps {
title: string;
onAction?: () => void;
}
export const ComponentName = (props: ComponentNameProps) => {
const { title, onAction } = props;
const { classes } = useStyles();
return (
<Stack className={classes.root} data-testid="componentNameRoot">
<Box className={classes.content} data-testid="componentNameContent">
<Typography variant="h6">{title}</Typography>
</Box>
</Stack>
);
};
Block Template
🎨 For style templates, see theming.instructions.md
Template for presentation blocks:
import React from "react";
import { Box, Typography, Button } from "@mui/material";
import { useStyles } from "./index.style";
interface BlockNameProps {
title: string;
description?: string;
onAction: () => void;
loading?: boolean;
}
export const BlockName = (props: BlockNameProps) => {
const { title, description, onAction, loading = false } = props;
const { classes } = useStyles();
return (
<Box className={classes.root} data-testid="blockNameRoot">
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
{description && (
<Typography variant="body2" className={classes.description}>
{description}
</Typography>
)}
<Button
variant="contained"
onClick={onAction}
disabled={loading}
data-testid="blockNameAction"
className={classes.actionButton}
>
{loading ? "Loading..." : "Action"}
</Button>
</Box>
);
};
Prohibitions
🎨 For styling prohibitions, see theming.instructions.md
Component Prohibitions
- ❌ NO default exports - Only named exports (
export const ComponentName) - ❌ NO class components - Only function components allowed
- ❌ NO relative imports for services/components - Use path aliases
- ❌ NO missing data-testid - All structural elements must have test IDs
Code Quality Prohibitions
- ❌ NO deep nesting - Keep component structure shallow with MUI layout components
- ❌ NO mixed concerns - Smart components handle logic, blocks handle presentation
- ❌ NO direct DOM manipulation - Use React patterns and MUI components
- ❌ NO missing accessibility - Interactive elements must have proper
aria-*attributes
Best Practices
When to Create Blocks
Create a block when:
- ✅ Component logic becomes complex (>50 lines)
- ✅ A piece of UI is reused in multiple places
- ✅ You want to isolate a specific UI concern
- ✅ Testing would benefit from component isolation
Props Design
- ✅ Use clear, descriptive prop names
- ✅ Provide default values where appropriate
- ✅ Use callback functions for actions (
onAction,onSubmit) - ✅ Pass primitive values when possible, avoid passing entire objects
State Management
- ✅ Keep state as close to where it's used as possible
- ✅ Lift state up only when shared between components
- ✅ Use smart components to manage business logic
- ✅ Keep blocks stateless for maximum reusability
Performance Considerations
- ✅ Avoid creating objects/functions in render methods
- ✅ Use React.memo for expensive pure components
- ✅ Consider useCallback for event handlers passed to blocks
Testing Strategy
- ✅ Test smart components for business logic
- ✅ Test blocks for rendering and user interactions
- ✅ Mock external dependencies in tests
- ✅ Each block should be testable in isolation
Naming & Casing
- ✅ Component folder and component name: PascalCase (
ComponentName) - ✅ CSS class keys: See theming.instructions.md for naming conventions
- ✅ data-testid: camelCase with component name prefix (
componentNameRoot) - ✅ Interface names: PascalCase with "Props" suffix (
ComponentNameProps)
Responsive & Layout Rules
- ✅ See theming.instructions.md for responsive design guidelines
- ✅ Prefer flex/stack/grid over absolute pixel positioning
- ✅ Use MUI layout components (
Stack,Box,Grid) for structure - ✅ Chat UI specifics: center initial view vertically and horizontally
- ✅ When input moves to bottom, use flex + sticky footer pattern
tsconfig.json Path Aliases
Ensure your tsconfig.json includes these path aliases:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@services/*": ["./services/*"],
"@components/*": ["./components/*"],
"@lib/*": ["./lib/*"]
}
}
}
Troubleshooting & Common Issues
Module Resolution Issues
Problem: Cannot find module './ComponentName' or its corresponding type declarations
Solution:
- Check file structure: Ensure both
index.tsxANDindex.style.tsexist - Verify exports: Use named exports
export const ComponentName = () => {} - Check barrel exports: Verify
blocks/index.tshas correct exports - Restart TypeScript: In VS Code: Cmd+Shift+P → "TypeScript: Restart TS Server"
- Clean build: Run
npm run buildto verify TypeScript compilation
// ✅ Correct block structure
blocks / ComponentName / index.tsx; // exports: export const ComponentName = () => {}
index.style.ts; // exports: export const useStyles = makeStyles()...
index.ts; // exports: export { ComponentName } from "./ComponentName";
Convex Integration Type Mismatches
Problem: Type 'UsePaginatedQueryResult<...>' is not assignable to type '{ messages: EnhancedMessage[] }'
Solution: Map Convex hook results to expected component props:
// ✅ Correct: Transform useThreadMessages result
const messagesResult = useThreadMessages(
api.chat.listThreadMessages as ThreadQuery,
currentThreadId ? { threadId: currentThreadId } : "skip",
{ initialNumItems: 50 },
);
// Transform to expected format
const transformedResult = {
messages: messagesResult?.results || [],
loadMore: messagesResult?.loadMore
? () => messagesResult.loadMore(10)
: undefined,
isLoadingMore: messagesResult?.isLoading,
hasLoadedAll: false, // Set based on your logic
};
Key Convex Hook Patterns:
useThreadMessagesreturns{ results, loadMore, isLoading }useMutationparameter names must match Convex function args exactly- Convex IDs must use
Id<"tableName">type, not strings
Parameter Naming Issues
Problem: Convex mutations fail due to parameter name mismatches
Solution: Always check the Convex schema and function definitions:
// ❌ Wrong parameter names
await uploadDocument({
name: file.name, // Should be 'fileName'
size: file.size, // Should be 'fileSize'
storageId, // Should be 'fileId'
});
// ✅ Correct parameter names (match Convex schema)
await uploadDocument({
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
fileId: storageId,
});
Build vs Development Server Discrepancies
Issue: Code builds successfully but shows errors in development
Solutions:
- Restart dev server: Stop (
Ctrl+C) and restart (npm run dev) - Clear Next.js cache:
rm -rf .next - Check for TypeScript cache: Restart TS server in VS Code
- Verify environment: Ensure
.env.localhas correct Convex URL
Authentication Context Issues
Problem: useConvexAuth() returns undefined or authentication state is inconsistent
Solution: Ensure proper provider hierarchy:
// ✅ Correct provider order
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ConvexClientProvider>
{" "}
{/* First: Convex client */}
<MuiThemeProvider>
{" "}
{/* Second: MUI theme */}
<SchematicProvider>
{" "}
{/* Third: App-specific providers */}
{children}
</SchematicProvider>
</MuiThemeProvider>
</ConvexClientProvider>
</body>
</html>
);
}
Agent Framework Integration
Problem: Agent responses not streaming or agent selection not working
Common Issues & Solutions:
-
Streaming not working:
// ✅ Ensure proper optimistic updates const sendMessage = useMutation( api.chat.sendMessageWithStreaming, ).withOptimisticUpdate( optimisticallySendMessage(api.chat.listThreadMessages as ThreadQuery), ); -
Agent type not persisting:
// ✅ Pass agentType to mutation await sendMessage({ threadId: targetThreadId, prompt: message, agentType: selectedAgent, // Include agent selection }); -
Document agent requires authentication:
// ✅ Check authentication before using document agent const { isAuthenticated } = useConvexAuth(); if (selectedAgent === "document" && !isAuthenticated) { // Handle unauthenticated document agent access setSelectedAgent("chat"); }
Performance Issues
Problem: Components re-rendering excessively or queries running too frequently
Solutions:
-
Memoize expensive computations:
const processedMessages = useMemo( () => messages.map((msg) => ({ ...msg, processed: true })), [messages], ); -
Use proper dependency arrays:
const handleSend = useCallback( (message: string) => { // handler logic }, [sendMessage], ); // Only depend on what changes -
Optimize Convex queries:
// ✅ Use skip for conditional queries const messagesResult = useThreadMessages( api.chat.listThreadMessages as ThreadQuery, currentThreadId ? { threadId: currentThreadId } : "skip", );
Debug Commands
# Check TypeScript compilation
npm run build
# Clear all caches
rm -rf .next node_modules/.cache
npm install
# Restart all services
pkill -f "npm\|next\|convex"
npm run dev
# Check Convex deployment status
npx convex dashboard
Migration Guide
When refactoring existing components:
- Identify Smart vs Blocks: Separate state/logic from presentation
- Extract Reusable Pieces: Look for repeated UI patterns
- Create Blocks: Move presentation logic to block components
- Update Imports: Use path aliases for cleaner imports
- Add Barrel Exports: Create
blocks/index.tsfor clean imports - Update Tests: Ensure each block can be tested independently
Component Development Checklist
Before Committing a UI Change
- Component folder structure correct
- Styling follows guidelines in theming.instructions.md
-
index.tsxuses proper component patterns - No inline styles present
-
data-testidattributes present on root/key nodes - Accessibility attributes present where needed
- Unit test stub exists (optional but preferred)
TypeScript Best Practices
- ✅ Use strict typing for all props and return values
- ✅ Define interfaces with descriptive names (
ComponentNameProps) - ✅ Use
const array: Array<T> = [...]for arrays - ✅ Use
const record: Record<KeyType, ValueType> = {...}for records - ✅ Add
as constfor string literals in discriminated unions
Remember: The goal is to create maintainable, reusable, and testable components that follow consistent patterns across the entire codebase.