Cursor Rules for Saitre Language App
Core Principles
- Small Files: No component, hook, or service file should exceed 300 lines. If it does, break it down.
- Single Responsibility: Each component, hook, and service should do ONE thing well.
- Separation of Concerns: Business logic in services/hooks, UI in components, utilities in utils.
- Documentation: Always explain "why", not just "what". Use JSDoc for all exported functions and components.
Component Guidelines
Component Size
- Maximum 300 lines per component file
- If a component exceeds 200 lines, consider breaking it into smaller sub-components
- Each sub-component should be 50-150 lines
Component Structure
- Components should ONLY handle UI rendering and user interactions
- Extract business logic to custom hooks or services
- Use composition: break large components into smaller, focused components
- One component = one responsibility
Component Documentation
Always include JSDoc for components:
/**
* ComponentName - Brief description of what it does
*
* @component
* @param {Object} props
* @param {Type} props.propName - Description and why it's needed
*
* Features:
* - Feature 1
* - Feature 2
*
* Usage:
* ```jsx
* <ComponentName propName={value} />
* ```
*/
State Management
useState Guidelines
- Maximum 5-7 useState hooks per component
- If you need more, use
useReducer for complex state
- Group related state into objects when appropriate
- Extract state management to custom hooks if it's complex
Custom Hooks
- Create custom hooks for reusable state logic
- Name hooks with
use prefix
- Document hooks with JSDoc explaining purpose and return values
State Updates
- Never update state based on props directly in render
- Use
useEffect with proper dependencies for prop-to-state sync
- Prefer controlled components over local state when possible
- Use
useMemo for expensive derived state
File Organization
Component Files
- One component per file
- File name matches component name (PascalCase)
- Place in appropriate directory:
components/{feature}/ComponentName.jsx
Service Files
- One service per domain (e.g.,
wordsService.js, sentencesService.js)
- Split if service exceeds 300 lines
- Separate CRUD operations from business logic
- Separate query operations from mutation operations
Utility Files
- Group related utilities together
- One utility file per domain (e.g.,
wordHelpers.js, validators.js)
- Split if file exceeds 300 lines
Code Quality
No Console.log in Production
Error Handling
- Services should throw typed errors (ValidationError, NotFoundError, etc.)
- Hooks should catch errors and return error state
- Components should display user-friendly error messages
- Always handle errors - never silently fail
Validation
- Centralize validation logic in
utils/validators.js
- Use same validators in both components and services
- Return structured validation results:
{ valid: boolean, error?: string }
Constants
- Extract magic numbers and strings to constants
- Create constant files:
constants/debounce.js, constants/batching.js, constants/ui.js
- Document why each constant has its value
Naming Conventions
Functions
- Event handlers:
handle* (e.g., handleSubmit, handleChange)
- Toggle functions:
toggle* or handleToggle* (e.g., toggleExpanded)
- Getter functions:
get* (e.g., getWordRoleIds)
- Transformer functions:
transform*, normalize*, format* (e.g., normalizeWord)
- Boolean state:
is*, has*, should*, show* (be consistent within component)
Variables
- Use descriptive names that explain purpose
- Avoid abbreviations unless widely understood
- Boolean variables should be clearly true/false (e.g.,
isLoading, not loading)
Files
- Components: PascalCase (e.g.,
WordForm.jsx)
- Hooks: camelCase with
use prefix (e.g., useWords.js)
- Services: camelCase (e.g.,
wordsService.js)
- Utils: camelCase (e.g.,
wordHelpers.js)
Documentation Requirements
Function Documentation
Always include JSDoc with:
- What the function does
- Why it exists (the problem it solves)
- Parameters with types and descriptions
- Return value with type and description
- Usage examples for complex functions
Example:
/**
* Aggregates word fields from meanings array for efficient Firestore queries
*
* WHY: Firestore doesn't support querying nested arrays (meanings[].tags).
* We pre-aggregate roleIds, tags, and search terms into top-level arrays
* to enable efficient queries like "find words with tag X".
*
* MUST be called on every create/update to keep aggregates in sync.
*
* @param {Object} wordData - Word data with meanings array
* @param {Array} wordData.meanings - Array of meaning objects
* @returns {Object} Aggregated fields: { roleIds: [], tags: [], search: [] }
*/
Complex Logic Documentation
- Explain algorithms and approaches
- Document edge cases and why they're handled
- Include performance considerations if relevant
React Best Practices
Component Composition
- Break UI into component hierarchy (see React's "Thinking in React" guide)
- Build static version first, then add interactivity
- Keep components focused on one visual/functional area
Props and State
- Identify minimal state (what changes over time)
- State should be owned by the closest common parent
- Pass data down via props, pass callbacks up for updates
- Don't store derived data in state - compute it
Performance
- Use
useMemo for expensive calculations
- Use
useCallback for functions passed to child components
- Memoize list items with
React.memo when appropriate
- Don't fetch more data than needed
Hooks
- Custom hooks should be reusable and focused
- One hook = one concern (data fetching, form state, etc.)
- Don't mix data fetching with mutations in same hook
Code Structure
Import Order
- React and React-related imports
- Third-party libraries
- Internal hooks
- Internal services
- Internal utils
- Internal components
- Types/interfaces (if using TypeScript)
- Constants
- Styles
Function Order in Components
- Component definition with props destructuring
- Hooks (useState, useEffect, useMemo, etc.)
- Event handlers
- Render helpers
- Early returns (loading, error states)
- Main render return
When Creating New Code
Before Writing
- Check if similar functionality exists
- Identify the single responsibility
- Plan component/hook/service structure
- Consider file size limits
While Writing
- Write JSDoc comments first
- Extract constants immediately
- Keep functions small (< 50 lines)
- Use descriptive variable names
- Add "why" comments for complex logic
After Writing
- Check file size (< 300 lines)
- Verify single responsibility
- Remove any console.log statements
- Add error handling
- Document complex logic
Refactoring Guidelines
When to Refactor
- File exceeds 300 lines
- Component has more than 7 useState hooks
- Function exceeds 50 lines
- Business logic in component
- Duplicated code appears
How to Refactor
- Identify the single responsibility
- Extract to smaller components/hooks/services
- Move business logic to appropriate layer
- Update documentation
- Test the refactored code
Anti-Patterns to Avoid
❌ DON'T
- Create components over 300 lines
- Mix business logic with UI components
- Use console.log in production code
- Create components with 10+ useState hooks
- Duplicate validation logic
- Write functions without JSDoc
- Use magic numbers/strings without constants
- Fetch all data when only some is needed
- Store derived data in state
- Silently catch errors without user feedback
✅ DO
- Break large components into smaller ones
- Extract business logic to hooks/services
- Use logging utility for debug messages
- Use useReducer for complex state
- Centralize validation logic
- Document all exported functions
- Extract constants with explanations
- Fetch only needed data
- Compute derived data with useMemo
- Show user-friendly error messages
Examples
Good Component Structure
/**
* WordCard - Displays a word card with basic info and actions
*
* @component
* @param {Object} props
* @param {Object} props.word - Word object to display
* @param {Function} props.onEdit - Callback when edit button clicked
* @param {Function} props.onDelete - Callback when delete button clicked
*/
export default function WordCard({ word, onEdit, onDelete }) {
const normalizedWord = useMemo(() => normalizeWord(word), [word]);
const roleIds = useMemo(() => getWordRoleIds(normalizedWord), [normalizedWord]);
if (!normalizedWord) return null;
return (
<div className="word-card">
{/* UI rendering */}
</div>
);
}
Good Hook Structure
/**
* useWord - Fetches a single word with real-time updates
*
* WHY: Provides consistent word fetching with loading/error states.
* Uses real-time listener for automatic updates when word changes.
*
* @param {string} wordId - ID of word to fetch
* @returns {Object} { word, loading, error }
*/
export function useWord(wordId) {
// Hook implementation
}
Good Service Structure
/**
* Creates a word in Firestore
*
* WHY: Centralizes word creation logic, ensures aggregates are calculated,
* and validates data before saving to prevent invalid data.
*
* @param {Object} wordData - Word data to create
* @returns {Promise<Object>} Created word object
* @throws {ValidationError} If word data is invalid
*/
export async function createWord(wordData) {
// Service implementation
}
Remember: When in doubt, break it down. Smaller, focused code is always better than large, complex code.