Skip to content

Instantly share code, notes, and snippets.

@dennisbochen-adtribute
Created June 12, 2025 07:13
Show Gist options
  • Select an option

  • Save dennisbochen-adtribute/8a5a3529fd914774d4bd71802279e7aa to your computer and use it in GitHub Desktop.

Select an option

Save dennisbochen-adtribute/8a5a3529fd914774d4bd71802279e7aa to your computer and use it in GitHub Desktop.
Best Practices

Development Best Practices

For LLM Reference: This directory contains comprehensive development best practices, coding standards, and workflow guidelines. Use these documents as authoritative references for all development decisions.

📚 Documentation Index

Core Development Practices

  • Purpose: Defines the standard technology stack and tooling decisions
  • Key Topics: TypeScript, React/Next.js, Hono, Cloud Run, Neon, TailwindCSS
  • Use Case: Reference when setting up new projects or making technology choices
  • Purpose: Mandatory coding standards and style guidelines
  • Key Topics: Naming conventions, variable practices, function design, programming paradigms
  • Use Case: Daily reference for code quality and consistency
  • Purpose: Prescriptive patterns for building state management with useReducer
  • Key Topics: File structure, schemas, actions, reducer patterns
  • Use Case: Building complex state management hooks

Workflow & Process

  • Purpose: Mandatory Git workflow, branching strategy, and pull request process
  • Key Topics: Branch naming, PR requirements, Loom recordings, mobile testing
  • Use Case: All code collaboration and version control activities
  • Purpose: Approach to documentation emphasizing self-documenting code
  • Key Topics: Minimal written docs, video documentation, API docs with Postman
  • Use Case: Deciding when and how to document code and processes

🎯 Quick Reference

For New Projects

  1. Start with: Technology Stack
  2. Follow: Code Style Guide
  3. Setup: Git Workflow

For Daily Development

For Documentation

🏗️ Architecture Principles

Technology Choices

  • TypeScript: Primary language for fast development and team navigation
  • Monorepo: Turbo-based with apps/ and packages/ structure
  • React/Next.js: Frontend applications with feature-based organization
  • Hono: Backend services deployed to Cloud Run containers
  • Neon: PostgreSQL database with proper migrations

Code Quality Standards

  • Self-Documenting: Code should be readable without extensive comments
  • Functional Programming: Prefer declarative over imperative approaches
  • Type Safety: Use Zod schemas over TypeScript enums
  • Immutability: Use const by default, avoid var entirely
  • Pure Functions: Minimize side effects for better testing

Development Workflow

  • Branch Strategy: feature/, fix/, chore/ with Jira ticket references
  • Pull Requests: Comprehensive descriptions with Loom recordings for significant changes
  • Mobile Testing: Mandatory testing on mobile viewports (320px minimum)
  • Code Reviews: Required approval before merging to master

🔄 State Management Patterns

useReducer Hook Structure

/hooks/use-[feature-name]/
├── index.ts           # Main hook implementation
├── actions.ts         # Action creators and handlers
├── reducer.ts         # Reducer function
├── schema.ts          # Type definitions and validation schemas
└── /schema/           # Additional schema files (optional)

Key Requirements

  • Zod Schemas: Both validation and state schemas
  • Discriminated Unions: For all action types
  • Separated Handlers: No inline logic in reducer
  • Immutable Updates: All state changes return new objects

📝 Documentation Strategy

Primary Approach

  1. Self-Documenting Code: Clear naming and structure
  2. Strategic Video Documentation: Complex features and processes
  3. API Documentation: Postman workspace with examples
  4. Minimal Written Docs: Only when absolutely necessary

When to Document

  • ✅ Complex business logic and algorithms
  • ✅ External API integrations
  • ✅ Architecture decisions and rationale
  • ✅ Public API contracts
  • ❌ Self-explanatory code
  • ❌ Implementation details
  • ❌ Redundant comments

🚀 Getting Started

For New Team Members

  1. Read through all best practice documents
  2. Set up development environment with required tools
  3. Review existing codebase examples
  4. Watch onboarding video documentation
  5. Practice with a small feature following all guidelines

For Existing Projects

  1. Audit current practices against these guidelines
  2. Create migration plan for any deviations
  3. Update CI/CD to enforce standards
  4. Train team on new or updated practices

🔧 Tools and Setup

Required Development Tools

  • Prettier: Code formatting (mandatory)
  • ESLint: Code linting and quality
  • TypeScript: Type checking and IntelliSense
  • Git: Version control with proper branching

Recommended IDE Configuration

  • Auto-format on save with Prettier
  • Real-time ESLint feedback
  • TypeScript strict mode enabled
  • File organization following established patterns

📊 Compliance and Quality

Code Review Checklist

  • Follows naming conventions
  • Uses appropriate data structures (Zod schemas)
  • Implements proper error handling
  • Includes necessary tests
  • Mobile viewport tested
  • Self-documenting without excessive comments

Project Health Indicators

  • Clean Git history with meaningful commits
  • Consistent code style across the codebase
  • Up-to-date dependencies and tooling
  • Comprehensive API documentation in Postman
  • Regular video documentation updates

Note: These best practices are living documents. They should be updated as the team learns and technology evolves. All changes should be discussed and approved by the development team.

Code Style Guide

For LLM Reference: This document defines mandatory coding standards and style guidelines. All code must conform to these standards for consistency and maintainability.

Naming Conventions

Case Conventions

  • camelCase: Variables, functions, methods, and properties
  • PascalCase: Classes, interfaces, types, and React components
  • SCREAMING_SNAKE_CASE: Constants and environment variables
  • kebab-case: File names, URLs, and CSS classes
// ✅ Correct naming
const userName = "john_doe";
const API_URL = "https://api.example.com";

class UserService {
  getUserData() {
    /* ... */
  }
}

interface UserProfile {
  displayName: string;
}

// ❌ Incorrect naming
const UserName = "john_doe";
const apiUrl = "https://api.example.com";

Variable Naming Best Practices

Use Meaningful Names

// ✅ Good - Clear intent
const userAccountBalance = 1250.0;
const hasPermissionToDelete = true;
const filteredActiveUsers = users.filter((user) => user.isActive);

// ❌ Bad - Unclear intent
const bal = 1250.0;
const flag = true;
const data = users.filter((user) => user.isActive);

Use Pronounceable Names

// ✅ Good - Easy to discuss
const createdTimestamp = Date.now();
const userRegistrationDate = new Date();

// ❌ Bad - Hard to pronounce/discuss
const crtdts = Date.now();
const usrregdt = new Date();

Use Searchable Names

// ✅ Good - Easy to find in codebase
const MAX_RETRY_ATTEMPTS = 3;
const DEFAULT_PAGE_SIZE = 20;

// ❌ Bad - Hard to search for
const retries = 3;
const size = 20;

Variable Declaration

Immutability Preference

// ✅ Preferred - Immutable by default
const userProfile = { name: "John", email: "john@example.com" };
const userList = ["Alice", "Bob", "Charlie"];

// ✅ Acceptable - When reassignment needed
let currentIndex = 0;
let isLoading = false;

// ❌ Never use - Outdated and error-prone
var userName = "john";

Rules for Variable Declarations

  1. Always use const when the variable won't be reassigned
  2. Use let only when reassignment is necessary
  3. Never use var - it has confusing scoping rules
  4. Declare variables close to their usage
  5. Initialize variables when declaring them

Function Design Principles

Single Responsibility

// ✅ Good - Single responsibility
function calculateTax(amount: number, rate: number): number {
  return amount * rate;
}

function formatCurrency(amount: number): string {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  }).format(amount);
}

// ❌ Bad - Multiple responsibilities
function calculateAndFormatTax(amount: number, rate: number): string {
  const tax = amount * rate;
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  }).format(tax);
}

Descriptive Function Names

// ✅ Good - Clear what the function does
function validateEmailAddress(email: string): boolean {
  /* ... */
}
function calculateMonthlyPayment(
  principal: number,
  rate: number,
  years: number
): number {
  /* ... */
}

// ❌ Bad - Unclear purpose
function check(email: string): boolean {
  /* ... */
}
function calc(p: number, r: number, y: number): number {
  /* ... */
}

Limited Function Arguments

// ✅ Good - Few parameters, clear purpose
function createUser(name: string, email: string): User {
  /* ... */
}

// ✅ Good - Use object for many parameters
interface CreateUserOptions {
  name: string;
  email: string;
  age?: number;
  department?: string;
  role?: string;
}

function createUserWithOptions(options: CreateUserOptions): User {
  /* ... */
}

// ❌ Bad - Too many parameters
function createUser(
  name: string,
  email: string,
  age: number,
  dept: string,
  role: string,
  active: boolean
): User {
  /* ... */
}

Pure Functions (Preferred)

// ✅ Excellent - Pure function
function addNumbers(a: number, b: number): number {
  return a + b;
}

// ✅ Good - Pure function with complex logic
function calculateDiscount(price: number, discountPercent: number): number {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error("Discount percent must be between 0 and 100");
  }
  return price * (discountPercent / 100);
}

// ❌ Avoid - Side effects make testing difficult
let globalCounter = 0;
function incrementAndReturn(): number {
  globalCounter++;
  console.log("Counter incremented");
  return globalCounter;
}

TypeScript Enum Alternatives

Use Zod over TypeScript Enums

// ✅ Preferred - Zod schema with runtime validation
import { z } from "zod";

export const userRoleSchema = z.enum(["admin", "user", "moderator"]);
export type UserRole = z.infer<typeof userRoleSchema>;

// Usage with validation
function assignRole(role: string): UserRole {
  return userRoleSchema.parse(role); // Runtime validation
}

// ❌ Avoid - TypeScript enum (compile-time only)
enum UserRole {
  Admin = "admin",
  User = "user",
  Moderator = "moderator",
}

Programming Paradigms

Functional Declarative over Imperative

Array Operations

// ✅ Preferred - Declarative functional approach
const activeUsers = users
  .filter((user) => user.isActive)
  .map((user) => ({
    id: user.id,
    name: user.name,
    lastLogin: user.lastLogin,
  }))
  .sort((a, b) => b.lastLogin.getTime() - a.lastLogin.getTime());

// ❌ Avoid - Imperative approach
const activeUsers = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].isActive) {
    activeUsers.push({
      id: users[i].id,
      name: users[i].name,
      lastLogin: users[i].lastLogin,
    });
  }
}
activeUsers.sort((a, b) => b.lastLogin.getTime() - a.lastLogin.getTime());

Object Transformations

// ✅ Preferred - Functional transformation
const updateUserProfile = (user: User, updates: Partial<User>): User => ({
  ...user,
  ...updates,
  updatedAt: new Date(),
});

// ❌ Avoid - Imperative mutation
function updateUserProfile(user: User, updates: Partial<User>): User {
  user.name = updates.name || user.name;
  user.email = updates.email || user.email;
  user.updatedAt = new Date();
  return user;
}

CSS and Styling

Container Queries over Media Queries

/* ✅ Preferred - Container queries for component-based responsive design */
.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 300px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

/* ❌ Less preferred - Media queries for global breakpoints */
@media (min-width: 768px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

Conditional Logic

Conditional Encapsulation

// ✅ Good - Encapsulated conditions
const canDeletePost = (user: User, post: Post): boolean => {
  return user.id === post.authorId || user.role === "admin";
};

if (canDeletePost(currentUser, selectedPost)) {
  deletePost(selectedPost.id);
}

// ❌ Less clear - Inline conditions
if (currentUser.id === selectedPost.authorId || currentUser.role === "admin") {
  deletePost(selectedPost.id);
}

Async Operations

Async/Await over Callbacks/Promises

// ✅ Preferred - Clean async/await
async function saveUserData(userData: UserData): Promise<User> {
  try {
    const validatedData = await validateUserData(userData);
    const savedUser = await database.users.create(validatedData);
    await sendWelcomeEmail(savedUser.email);
    return savedUser;
  } catch (error) {
    logger.error("Failed to save user data:", error);
    throw error;
  }
}

// ❌ Avoid - Promise chains
function saveUserData(userData: UserData): Promise<User> {
  return validateUserData(userData)
    .then((validatedData) => database.users.create(validatedData))
    .then((savedUser) => {
      return sendWelcomeEmail(savedUser.email).then(() => savedUser);
    })
    .catch((error) => {
      logger.error("Failed to save user data:", error);
      throw error;
    });
}

Code Organization

Group Related Code

// ✅ Good - Related functions grouped together
class UserService {
  // User creation
  async createUser(userData: CreateUserData): Promise<User> {
    /* ... */
  }
  private validateUserData(userData: CreateUserData): void {
    /* ... */
  }
  private hashPassword(password: string): string {
    /* ... */
  }

  // User retrieval
  async getUserById(id: string): Promise<User | null> {
    /* ... */
  }
  async getUserByEmail(email: string): Promise<User | null> {
    /* ... */
  }

  // User updates
  async updateUser(id: string, updates: Partial<User>): Promise<User> {
    /* ... */
  }
  async updateUserPassword(id: string, newPassword: string): Promise<void> {
    /* ... */
  }
}

Consistent File Organization

/src/features/user-management/
├── components/
│   ├── UserList.tsx
│   ├── UserCard.tsx
│   └── UserForm.tsx
├── hooks/
│   ├── useUsers.ts
│   └── useUserForm.ts
├── services/
│   └── userService.ts
├── types/
│   └── user.types.ts
└── utils/
    └── userValidation.ts

Documentation Philosophy

For LLM Reference: This document outlines our approach to documentation, emphasizing self-documenting code over extensive written documentation, with strategic use of video documentation and API documentation tools.

Core Philosophy

Self-Documenting Code First

Our primary documentation strategy is self-documenting code. Well-written code with clear naming, structure, and minimal complexity should tell its own story without requiring extensive external documentation.

Minimal Written Documentation

We intentionally minimize written documentation in favor of:

  1. Clear, readable code
  2. Meaningful naming conventions
  3. Logical code organization
  4. Strategic video documentation
  5. API documentation tools

Self-Documenting Code Principles

1. Meaningful Names

// ✅ Self-documenting - Purpose is clear from naming
function calculateMonthlyPayment(
  principal: number,
  annualInterestRate: number,
  loanTermInYears: number
): number {
  const monthlyRate = annualInterestRate / 12 / 100;
  const numberOfPayments = loanTermInYears * 12;

  return (
    (principal * monthlyRate * Math.pow(1 + monthlyRate, numberOfPayments)) /
    (Math.pow(1 + monthlyRate, numberOfPayments) - 1)
  );
}

// ❌ Requires documentation - Purpose unclear
function calc(p: number, r: number, t: number): number {
  const mr = r / 12 / 100;
  const n = t * 12;
  return (p * mr * Math.pow(1 + mr, n)) / (Math.pow(1 + mr, n) - 1);
}

2. Clear Code Structure

// ✅ Self-documenting through structure
class UserService {
  // User Creation
  async createUser(userData: CreateUserRequest): Promise<User> {
    const validatedData = this.validateUserData(userData);
    const hashedPassword = await this.hashPassword(userData.password);
    const newUser = await this.saveUserToDatabase({
      ...validatedData,
      password: hashedPassword,
    });
    await this.sendWelcomeEmail(newUser.email);
    return newUser;
  }

  // User Retrieval
  async findUserByEmail(email: string): Promise<User | null> {
    return this.database.users.findUnique({ where: { email } });
  }

  // Private helpers - implementation details
  private validateUserData(userData: CreateUserRequest): ValidatedUserData {
    // Validation logic
  }

  private async hashPassword(password: string): Promise<string> {
    // Password hashing logic
  }
}

3. Expressive Business Logic

// ✅ Business rules are clear from code structure
class SubscriptionService {
  canUpgradeSubscription(user: User, targetPlan: SubscriptionPlan): boolean {
    const hasActiveSubscription = user.subscription?.status === "active";
    const isUpgrade = targetPlan.price > user.subscription?.plan.price;
    const hasValidPaymentMethod = user.paymentMethods.some((pm) => pm.isValid);

    return hasActiveSubscription && isUpgrade && hasValidPaymentMethod;
  }

  calculateProrationAmount(
    currentPlan: Plan,
    newPlan: Plan,
    daysRemaining: number
  ): number {
    const dailyDifference = (newPlan.price - currentPlan.price) / 30;
    return dailyDifference * daysRemaining;
  }
}

When to Write Documentation

Required Documentation

Write documentation only when:

  1. Complex Business Logic: Algorithms or business rules that aren't immediately obvious
  2. External Dependencies: Third-party integrations with specific requirements
  3. Architecture Decisions: High-level system design choices and rationale
  4. API Contracts: Public API documentation for external consumers

Documentation That Should Be Avoided

Don't write documentation for:

  1. Obvious Code: Self-explanatory functions and variables
  2. Implementation Details: How the code works (should be clear from reading it)
  3. Redundant Comments: Restating what the code already says
  4. Outdated Information: Documentation that becomes stale quickly
// ❌ Unnecessary documentation - code is self-explanatory
/**
 * Gets the user's full name by concatenating first and last name
 * @param firstName - The user's first name
 * @param lastName - The user's last name
 * @returns The user's full name
 */
function getFullName(firstName: string, lastName: string): string {
  return `${firstName} ${lastName}`;
}

// ✅ No documentation needed - code is clear
function getFullName(firstName: string, lastName: string): string {
  return `${firstName} ${lastName}`;
}

Video Documentation Library

Strategic Video Documentation

Maintain a curated library of video documentation for:

  1. Complex Features: New feature walkthroughs and explanations
  2. System Architecture: High-level system design and interactions
  3. Development Processes: Setup, deployment, and workflow explanations
  4. Troubleshooting: Common issues and their solutions

Video Documentation Standards

Content Guidelines

  • Duration: 5-10 minutes maximum per video
  • Focus: One topic per video
  • Quality: Clear audio, readable screen capture
  • Structure: Introduction, main content, summary/next steps

Video Categories

/video-documentation/
├── features/          # Feature demonstrations
├── architecture/      # System design explanations
├── processes/         # Development workflow
├── troubleshooting/   # Problem resolution
└── onboarding/       # New team member resources

Maintenance

  • Regular Review: Quarterly review of video relevance
  • Update or Remove: Outdated videos should be updated or removed
  • Version Control: Tag videos with relevant software versions

Video Creation Workflow

  1. Plan Content: Outline key points before recording
  2. Record Demo: Use Loom or similar screen recording tool
  3. Edit if Necessary: Basic editing for clarity
  4. Catalog: Add to video documentation library with clear title and description
  5. Share: Make accessible to relevant team members

API Documentation

Postman Workspace

Maintain comprehensive API documentation using Postman workspace:

Organization Structure

Postman Workspace/
├── Collections/
│   ├── Authentication APIs
│   ├── User Management APIs
│   ├── Payment APIs
│   └── Admin APIs
├── Environments/
│   ├── Development
│   ├── Staging
│   └── Production
└── Documentation/
    ├── Getting Started
    ├── Authentication Guide
    └── Error Handling

Documentation Standards

  • Complete Examples: Include request/response examples for all endpoints
  • Error Scenarios: Document common error responses
  • Authentication: Clear auth requirements for each endpoint
  • Environment Variables: Use variables for different environments

Maintenance

  • Keep Updated: API docs must stay current with code changes
  • Test Examples: Regularly verify example requests work
  • Team Access: Ensure all developers have workspace access

Documentation Maintenance

Responsibilities

  • Developers: Ensure code is self-documenting
  • Tech Leads: Review and approve video documentation
  • Product Team: Maintain API documentation accuracy

Review Process

  1. Code Reviews: Include documentation review as part of PR process
  2. Quarterly Cleanup: Remove outdated documentation
  3. User Feedback: Collect feedback on documentation usefulness

Quality Metrics

  • Code Clarity: Can new team members understand code without explanation?
  • Video Relevance: Are videos helping solve real problems?
  • API Accuracy: Do API examples work as documented?

Tools and Platforms

Recommended Tools

  • Loom: Video documentation and screen recording
  • Postman: API documentation and testing
  • GitHub: Code organization and PR descriptions
  • Miro/Figma: Architecture diagrams when necessary

Integration with Development Workflow

  • PR Templates: Include documentation impact assessment
  • Definition of Done: Include documentation requirements
  • Code Reviews: Evaluate code clarity and self-documentation

Best Practices

Code Documentation

  1. Write code that explains itself
  2. Use meaningful variable and function names
  3. Structure code logically with clear separation of concerns
  4. Extract complex logic into well-named functions
  5. Use TypeScript types to document data structures

Video Documentation

  1. Keep videos short and focused
  2. Update or remove outdated content
  3. Use consistent recording setup and quality
  4. Include clear titles and descriptions
  5. Make videos easily discoverable

API Documentation

  1. Keep examples current and working
  2. Include both success and error scenarios
  3. Use realistic test data
  4. Document authentication requirements clearly
  5. Organize logically by feature or domain

Anti-Patterns to Avoid

Over-Documentation

  • Writing documentation for self-explanatory code
  • Maintaining documentation that duplicates what the code already says
  • Creating documentation that quickly becomes outdated

Under-Documentation

  • Skipping API documentation for public endpoints
  • Not explaining complex business logic or algorithms
  • Ignoring video documentation for complex features

Poor Maintenance

  • Allowing documentation to become outdated
  • Not removing obsolete documentation
  • Failing to update examples when APIs change

Form and Input Implementation Best Practices

For LLM Reference: This document provides prescriptive patterns for implementing forms and inputs in React applications. These patterns are extracted from the @/edit-menu feature.

Quick Reference

Required Patterns Summary

  • Use controlled inputs with onChange handlers for immediate state updates
  • Implement proper accessibility with labels, aria-describedby, and unique IDs
  • Apply debouncing for expensive operations (rich text editors, API calls)
  • Use Zod schemas for validation with safeParse pattern
  • Implement proper loading states and error handling
  • Follow component composition patterns for reusable form elements
  • Use TypeScript for type safety throughout form implementations

Basic Input Implementation

Standard Input Pattern

<div className='flex flex-col gap-y-2'>
  <label className='font-semibold' htmlFor={`input-${uniqueId}`}>
    Field Label
  </label>
  <p
    className='text-xs text-neutral-700 mb-2'
    id={`aria-described-by-${uniqueId}`}
  >
    Helpful description of what this field is for
  </p>
  <Input
    id={`input-${uniqueId}`}
    name={`input-${uniqueId}`}
    aria-describedby={`aria-described-by-${uniqueId}`}
    defaultValue={initialValue}
    onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
      updateState({
        id: itemId,
        data: { fieldName: e.target.value },
      })
    }
  />
</div>

Input Rules

  1. Always provide unique IDs using template literals with entity IDs
  2. Always include labels with proper htmlFor attributes
  3. Always include descriptive text with aria-describedby linkage
  4. Use controlled components with onChange handlers
  5. Pass typed event handlers with proper TypeScript annotations

Rich Text Editor Pattern

TipTap Editor with Debouncing

export function TextEditor({
  onChange,
  defaultContent,
}: {
  onChange: (value: string) => void;
  defaultContent?: string;
}) {
  const editor = useEditor({
    extensions: [StarterKit, Underline],
    content: defaultContent,
    editorProps: {
      attributes: {
        class: "h-[180px] overflow-y-auto focus:outline-none",
      },
    },
  });

  const editorContent = editor?.getHTML();

  // Debounce expensive operations
  const [, cancel] = useDebounce(
    () => {
      onChange(editorContent || "");
    },
    1000, // 1 second debounce
    [editorContent]
  );

  const editorActions = [
    {
      name: "bold",
      icon: <BoldIcon className='size-full' />,
      label: "Bold",
      isActive: () => editor?.isActive("bold"),
      onClick: () => editor?.chain().focus().toggleBold().run(),
    },
  ];

  return (
    <div className='w-full h-full bg-white rounded-2xl ring-1 ring-neutral-200'>
      <div className='bg-neutral-50 rounded-t-xl p-2 flex gap-x-2'>
        {editorActions.map((action) => (
          <button
            onClick={action.onClick}
            className={cn(
              "px-2 py-1 rounded-md",
              action.isActive()
                ? "bg-gray-200 hover:bg-gray-300"
                : "hover:bg-gray-200"
            )}
            key={action.name}
            aria-label={action.label}
            title={action.label}
          >
            <span className='sr-only'>{action.label}</span>
            <div className='size-4'>{action.icon}</div>
          </button>
        ))}
      </div>
      <EditorContent
        editor={editor}
        className='px-4 py-2 inline-block min-h-[200px]'
      />
    </div>
  );
}

Rich Text Rules

  1. Always implement debouncing for onChange callbacks (1000ms recommended)
  2. Provide accessible toolbar buttons with aria-label and title
  3. Use screen reader text with sr-only class
  4. Configure editor props with proper focus management

Dropdown/Select Patterns

Combobox with Search

export function CategoryCombobox({
  categories,
  selectedCategoryIds,
  onRemove,
  onAdd,
}: {
  categories: Category[];
  selectedCategoryIds: string[];
  onRemove: (args: { categoryId: string }) => void;
  onAdd: (args: { categoryId: string }) => void;
}) {
  const [open, setOpen] = React.useState(false);

  return (
    <div className='inline-flex justify-between'>
      <div className='inline-flex flex-1 justify-left gap-2'>
        {selectedCategoryIds.map((id) => (
          <button
            key={id}
            onClick={() => onRemove({ categoryId: id })}
            className='rounded-lg px-3 py-1.5 text-sm font-medium bg-neutral-700 hover:bg-neutral-600 transition-colors text-white'
          >
            {categories.find((category) => category.id === id)?.name}
          </button>
        ))}
      </div>

      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button
            variant='outline'
            role='combobox'
            aria-expanded={open}
            size='sm'
          >
            Add to category
            <PlusIcon className='h-4 w-4' />
          </Button>
        </PopoverTrigger>

        <PopoverContent className='w-72 p-0'>
          <Command>
            <CommandInput placeholder='Search for category...' />
            <CommandList>
              <CommandEmpty>No category found.</CommandEmpty>
              <CommandGroup>
                {categories.map((category) => (
                  <CommandItem
                    key={category.id}
                    value={category.id}
                    onSelect={(currentValue) => {
                      selectedCategoryIds.includes(category.id)
                        ? onRemove({ categoryId: currentValue })
                        : onAdd({ categoryId: currentValue });
                    }}
                  >
                    <Check
                      className={cn(
                        "mr-2 h-4 w-4",
                        selectedCategoryIds.includes(category.id)
                          ? "opacity-100"
                          : "opacity-0"
                      )}
                    />
                    {category.name}
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandList>
          </Command>
        </PopoverContent>
      </Popover>
    </div>
  );
}

Form Validation Patterns

Server Action Validation with Zod

"use server";

import { z } from "zod";

export async function updateMenuName(args: { menuId: string; name: string }) {
  try {
    // Always validate input with safeParse
    const validatedFields = z
      .object({
        menuId: z.string(),
        name: z.string().min(1, "Name is required"),
      })
      .safeParse({
        menuId: args.menuId,
        name: args.name,
      });

    // Handle validation errors
    if (!validatedFields.success) {
      return {
        success: false,
        error: "Invalid menu data",
      };
    }

    // Use validated data
    const result = await db.update({
      id: validatedFields.data.menuId,
      name: validatedFields.data.name,
    });

    return {
      success: true,
      data: result,
    };
  } catch (error) {
    return {
      success: false,
      error: "Sorry...we couldn't update your menu. Please try again.",
    };
  }
}

Client-Side Schema

import { z } from "zod";

export const itemSchema = z.object({
  name: z.string().min(1, "Name is required"),
  description: z.string().optional(),
  price: z
    .string()
    .regex(
      /^\d+(\.\d{1,2})?$/,
      "Price must be a valid decimal with up to 2 decimal places"
    )
    .optional(),
  hidden: z.boolean().default(false),
});

export type Item = z.infer<typeof itemSchema>;

Validation Rules

  1. Always use safeParse instead of parse for user input
  2. Define clear error messages in schema validation
  3. Return consistent response format with success/error properties
  4. Handle both validation and runtime errors separately

State Management with Local Storage

useLocalStorageReducer Hook

export function useLocalStorageReducer<
  TState,
  TAction extends { type: string; payload?: any },
>(
  reducer: Reducer<TState, TAction>,
  initialState: TState,
  key: string
): [TState, React.Dispatch<TAction>, () => void] {
  const getInitialState = (): TState => {
    try {
      if (typeof window === "undefined") return initialState;
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as TState) : initialState;
    } catch (error) {
      console.error("Error reading from localStorage:", error);
      return initialState;
    }
  };

  const [state, dispatch] = useReducer(reducer, getInitialState());

  useEffect(() => {
    try {
      if (typeof window === "undefined") return;
      window.localStorage.setItem(key, JSON.stringify(state));
    } catch (error) {
      console.error("Error writing to localStorage:", error);
    }
  }, [state, key]);

  function clearLocalStorage() {
    window.localStorage.removeItem(key);
  }

  return [state, dispatch, clearLocalStorage];
}

Form Layout Patterns

Tab-Based Form Layout

export function EditLayout({
  titleComponent,
  tabs,
  topRightComponent,
}: {
  titleComponent: React.ReactNode;
  topRightComponent?: React.ReactNode;
  tabs: Array<{
    id: string;
    name: string;
    component: React.ReactNode;
    ptid: string;
  }>;
}) {
  return (
    <div className='h-full w-full'>
      <div className='rounded-2xl h-full w-full bg-white overflow-y-auto'>
        <Tabs defaultValue={tabs[0]?.id} className='w-full'>
          <div className='sticky top-0 bg-white/60 backdrop-blur-md z-10'>
            <div className='p-4 inline-flex justify-between w-full'>
              <div className='flex-1'>{titleComponent}</div>
              <div className='flex-1'>{topRightComponent}</div>
            </div>
            <TabsList>
              {tabs.map((tab) => (
                <TabsTrigger value={tab.id} key={tab.id} data-ptid={tab.ptid}>
                  {tab.name}
                </TabsTrigger>
              ))}
            </TabsList>
          </div>
          {tabs.map((tab) => (
            <TabsContent value={tab.id} key={tab.id}>
              {tab.component}
            </TabsContent>
          ))}
        </Tabs>
      </div>
    </div>
  );
}

Accessibility Requirements

Mandatory ARIA and Labeling

// Always link labels to inputs
<label htmlFor={`input-${uniqueId}`}>Field Label</label>
<Input
  id={`input-${uniqueId}`}
  aria-describedby={`description-${uniqueId}`}
/>

// Provide helpful descriptions
<p id={`description-${uniqueId}`}>
  Describe what this field is for
</p>

// Screen reader text for icons
<button aria-label="Remove item" title="Remove item">
  <span className="sr-only">Remove item</span>
  <TrashIcon className="w-4 h-4" />
</button>

// Proper ARIA roles for complex inputs
<Button
  role="combobox"
  aria-expanded={open}
>
  Select option
</Button>

Performance Optimization

Debouncing Patterns

// For expensive operations (API calls, complex calculations)
const [debouncedValue] = useDebounce(inputValue, 1000);

// For rich text editors
const [, cancel] = useDebounce(
  () => {
    onChange(editorContent || "");
  },
  1000,
  [editorContent]
);

// For search inputs
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);

Performance Rules

  1. Debounce expensive operations (1000ms for API calls, 300ms for search)
  2. Implement optimistic updates for better UX
  3. Handle rollback scenarios on server errors
  4. Minimize re-renders with proper dependency arrays

Summary Checklist

When implementing forms and inputs, ensure:

  • Accessibility: Labels, ARIA attributes, keyboard navigation
  • Type Safety: TypeScript interfaces, Zod validation schemas
  • State Management: Proper state updates, local storage if needed
  • Performance: Debouncing, optimistic updates, minimal re-renders
  • Error Handling: Validation, server errors, user feedback
  • Testing: Test IDs, clear error states, observable async states
  • User Experience: Loading states, clear feedback, intuitive interactions

Git Workflow & Branching Strategy

For LLM Reference: This document defines the mandatory Git workflow, branching strategy, and pull request process. All team members must follow these procedures for consistent collaboration.

Branching Model

Main Branch

  • Branch Name: master
  • Purpose: Production-ready code
  • Protection: All changes must go through pull requests
  • Direct Commits: Not allowed (except for emergency hotfixes)

Branch Types and Naming Conventions

Feature Branches

Format: feature/[JIRA-TICKET]-[brief-description]

# Examples
feature/MEN-123-user-authentication
feature/MEN-456-dashboard-redesign
feature/MEN-789-payment-integration

Purpose: New features and enhancements Lifetime: Created from master, merged back to master Cleanup: Delete after successful merge

Bug Fix Branches

Format: fix/[JIRA-TICKET]-[brief-description]

# Examples
fix/MEN-234-login-redirect-issue
fix/MEN-567-payment-form-validation
fix/MEN-890-mobile-responsive-layout

Purpose: Bug fixes and corrections Lifetime: Created from master, merged back to master Cleanup: Delete after successful merge

Chore Branches

Format: chore/[JIRA-TICKET]-[brief-description]

# Examples
chore/MEN-111-update-dependencies
chore/MEN-222-refactor-user-service
chore/MEN-333-setup-ci-pipeline

Purpose: Maintenance tasks, refactoring, dependency updates Lifetime: Created from master, merged back to master Cleanup: Delete after successful merge

Branch Creation Workflow

1. Start from Master

# Ensure you're on master and up-to-date
git checkout master
git pull origin master

2. Create Feature Branch

# Create and checkout new branch
git checkout -b feature/MEN-123-user-authentication

# Or create branch and push to remote
git checkout -b feature/MEN-123-user-authentication
git push -u origin feature/MEN-123-user-authentication

3. Regular Development

# Make commits with descriptive messages
git add .
git commit -m "Add user authentication middleware"

# Push changes regularly
git push origin feature/MEN-123-user-authentication

Commit Message Standards

Format

[Type]: Brief description of changes

Optional longer description explaining what and why.

- Bullet points for multiple changes
- Reference Jira ticket: MEN-123

Commit Types

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code style changes (formatting, etc.)
  • refactor: Code refactoring
  • test: Adding or updating tests
  • chore: Maintenance tasks

Examples

git commit -m "feat: Add user authentication middleware

Implements JWT-based authentication for API endpoints.
Includes middleware for token validation and user context.

- Add JWT token validation
- Create user context middleware
- Update API route protection

Closes MEN-123"

Pull Request Process

Required Elements for Pull Requests

1. Descriptive Title

[MEN-123] Add user authentication system

2. Comprehensive Description

## Summary

Implements JWT-based authentication system for the application.

## Changes Made

- Added authentication middleware
- Implemented token validation
- Created user context provider
- Updated API route protection

## Testing

- [ ] Unit tests pass
- [ ] Integration tests pass
- [ ] Manual testing completed
- [ ] Mobile viewport tested

## Jira Ticket

MEN-123: Implement User Authentication

3. Loom Recording (Required for Substantial Changes)

  • When Required: Any PR with significant UI changes or complex functionality
  • Content: Screen recording demonstrating the changes
  • Duration: Keep under 5 minutes
  • Include: Before/after comparison, key functionality walkthrough
## Demo Video

🎥 [Loom Recording](https://loom.com/share/your-recording-id)

Demonstrates:

- New authentication flow
- Error handling
- Mobile responsiveness

Testing Requirements

Before Creating PR

  • All unit tests pass
  • Integration tests pass
  • Manual testing completed on desktop
  • Mobile viewport testing completed (mandatory)
  • No console errors
  • Code follows style guidelines

Mobile Testing Checklist

  • Layout works on mobile devices (320px minimum width)
  • Touch targets are appropriately sized
  • Navigation is accessible on mobile
  • Forms are mobile-friendly
  • Performance is acceptable on mobile networks

Code Review Process

Review Requirements

  • Minimum Reviews: 1 approving review required
  • Reviewers: Assign relevant team members
  • Self-Review: Review your own PR before requesting reviews

Review Checklist for Reviewers

  • Code follows established patterns
  • Logic is sound and efficient
  • Error handling is appropriate
  • Tests are comprehensive
  • Documentation is updated if needed
  • No security vulnerabilities
  • Performance considerations addressed

Addressing Review Feedback

# Make requested changes
git add .
git commit -m "fix: Address review feedback - update error handling"
git push origin feature/MEN-123-user-authentication

Merge Process

Before Merging

  1. All checks pass: CI/CD pipeline, tests, linting
  2. Reviews approved: Required approvals received
  3. Conflicts resolved: No merge conflicts with master
  4. Branch updated: Rebase or merge latest master if needed

Merge Strategy

Preferred: Squash and merge

  • Keeps master history clean
  • Combines all commits into single commit
  • Retains PR reference

After Merge

# Switch to master and pull latest
git checkout master
git pull origin master

# Delete local feature branch
git branch -d feature/MEN-123-user-authentication

# Delete remote feature branch (if not auto-deleted)
git push origin --delete feature/MEN-123-user-authentication

Emergency Hotfixes

Hotfix Process

  1. Create hotfix branch from master

    git checkout master
    git checkout -b hotfix/critical-security-fix
  2. Make minimal necessary changes

  3. Test thoroughly

  4. Create emergency PR with "HOTFIX" label

  5. Get expedited review

  6. Merge immediately after approval

Hotfix Naming

hotfix/[brief-description]
hotfix/security-vulnerability-patch
hotfix/payment-gateway-outage

Branch Protection Rules

Master Branch Protection

  • ✅ Require pull request reviews before merging
  • ✅ Dismiss stale PR approvals when new commits are pushed
  • ✅ Require status checks to pass before merging
  • ✅ Require branches to be up to date before merging
  • ✅ Require linear history (optional, team preference)
  • ✅ Include administrators in these restrictions

Best Practices

General Guidelines

  1. Keep branches focused - One feature/fix per branch
  2. Regular updates - Pull master changes regularly
  3. Clean history - Use meaningful commit messages
  4. Test before PR - Don't rely on CI to catch basic issues
  5. Small PRs - Easier to review and less risk

Branch Hygiene

  1. Delete merged branches - Keep repository clean
  2. Regular cleanup - Remove stale local branches
  3. Sync frequently - Don't let branches get too far behind master
  4. Rebase when appropriate - Keep history linear when possible

Communication

  1. Link Jira tickets - Always reference relevant tickets
  2. Descriptive PR titles - Make intent clear
  3. Comprehensive descriptions - Help reviewers understand changes
  4. Responsive to feedback - Address review comments promptly

React useReducer State Management Hook Best Practices

For LLM Reference: This document provides prescriptive patterns for building state management hooks with useReducer. Follow these patterns exactly when implementing new hooks or refactoring existing ones.

Quick Reference

Required File Structure

/hooks/use-[feature-name]/
├── index.ts           # Main hook implementation
├── actions.ts         # Action creators and handlers
├── reducer.ts         # Reducer function
├── schema.ts          # Type definitions and validation schemas
└── /schema/           # Additional schema files (optional)

Implementation Checklist

  • Zod schemas for validation and runtime types
  • Discriminated union types for all actions
  • Separated action handler functions (not inline in reducer)
  • Immutable state updates in all handlers
  • TypeScript types exported from schema files
  • Hook returns both state and action creators
  • Tests for each action handler function
  • Default state defined and applied

Core Patterns

This guide outlines mandatory patterns for building robust, scalable state management hooks using React's useReducer. These patterns ensure consistency, type safety, and maintainability across all implementations.

File Structure & Organization

Mandatory Directory Structure

/hooks/use-[feature-name]/
├── index.ts           # Main hook implementation
├── actions.ts         # Action creators and handlers
├── reducer.ts         # Reducer function
├── schema.ts          # Type definitions and validation schemas
└── /schema/           # Additional schema files (if complex)
    ├── schema.ts
    ├── validation-schema.ts
    └── shared.ts

File Responsibilities (Must Follow)

  • index.ts: Hook interface, useReducer setup, action creators, validation
  • actions.ts: Action type definitions and handler functions
  • reducer.ts: Main reducer with switch statement only
  • schema.ts: Zod schemas and TypeScript type exports

Schema & Type Safety

Pattern: Dual Schema Approach (Required)

ALWAYS implement both schemas:

// schema.ts - MANDATORY TEMPLATE
import { z } from 'zod';

// 1. STRICT validation schema (for final validation)
export const validationSchema = z.object({
  [field]: z.string(),                    // Required fields
  [nested]: z.object({
    [field]: z.string(),
  }),
});

// 2. PERMISSIVE state schema (for runtime state)
export const stateSchema = z.object({
  [field]: z.string().nullish(),          // Optional/nullable fields
  [nested]: z.object({
    [field]: z.string().nullish(),
  }).nullish(),
});

// 3. ALWAYS export the state type
export type [FeatureName]State = z.infer<typeof stateSchema>;

// 4. ALWAYS export all component types
export type [ComponentType] = z.infer<typeof [componentSchema]>;

Rules for Schema Design

  1. validationSchema: Use for final validation before API calls
  2. stateSchema: Use for hook state management (more permissive)
  3. All fields in stateSchema must be .nullish() or optional
  4. Export every type that components will use
  5. Use consistent naming: [FeatureName]State, [Component]Type

Actions Pattern

Template: Action Definition (MANDATORY STRUCTURE)

// actions.ts - FOLLOW THIS EXACT PATTERN

// 1. ALWAYS define individual action types
export type Add[Entity]Action = {
    type: 'add-[entity]';
    payload: { [entity]: [EntityType] };
};

export type Update[Entity]Action = {
    type: 'update-[entity]';
    payload: { id: string; updates: Partial<[EntityType]> };
};

export type Remove[Entity]Action = {
    type: 'remove-[entity]';
    payload: { id: string };
};

// 2. ALWAYS create discriminated union
export type Actions =
    | Add[Entity]Action
    | Update[Entity]Action
    | Remove[Entity]Action;

// 3. ALWAYS create separate handler functions
export function add[Entity](state: [FeatureName]State, action: Add[Entity]Action): [FeatureName]State {
    return {
        ...state,
        [entities]: [...(state.[entities] || []), action.payload.[entity]]
    };
}

export function update[Entity](state: [FeatureName]State, action: Update[Entity]Action): [FeatureName]State {
    return {
        ...state,
        [entities]: (state.[entities] || []).map(item =>
            item.id === action.payload.id
                ? { ...item, ...action.payload.updates }
                : item
        )
    };
}

export function remove[Entity](state: [FeatureName]State, action: Remove[Entity]Action): [FeatureName]State {
    return {
        ...state,
        [entities]: (state.[entities] || []).filter(item => item.id !== action.payload.id)
    };
}

Action Naming Rules (Must Follow)

  1. Action types: Use kebab-case: 'add-item', 'update-filter'
  2. Action interfaces: Use PascalCase + "Action": AddItemAction
  3. Handler functions: Use camelCase matching entity: addItem, updateFilter
  4. Always use payload property for action data
  5. Handler functions must be pure (no side effects)
  6. Always return new state objects (immutable updates)

Reducer Implementation

Template: Reducer Function (EXACT PATTERN)

// reducer.ts - FOLLOW THIS EXACT STRUCTURE
import { type [FeatureName]State } from './schema';
import {
    Actions,
    add[Entity],
    update[Entity],
    remove[Entity],
    // Import ALL action handlers
} from './actions';

export function reducer(state: [FeatureName]State, action: Actions): [FeatureName]State {
    switch (action.type) {
        case 'add-[entity]':
            return add[Entity](state, action);
        case 'update-[entity]':
            return update[Entity](state, action);
        case 'remove-[entity]':
            return remove[Entity](state, action);
        // Add case for EVERY action type
        default:
            return state;
    }
}

Reducer Rules (Non-Negotiable)

  1. Import all action handlers - never define logic inline
  2. Switch statement only - no other logic in reducer
  3. Always include default case returning current state
  4. One case per action type - no shared cases
  5. Never mutate state - always return new objects
  6. Import types from schema file - maintain separation

Main Hook Implementation

Template: Hook Structure (MANDATORY PATTERN)

// index.ts - FOLLOW THIS EXACT TEMPLATE
'use client';

import { useEffect, useReducer, useState, useMemo, useCallback } from 'react';
import { reducer } from './reducer';
import { type [FeatureName]State, validationSchema, stateSchema } from './schema';
import { formatZodErrors } from '@/lib/utils/zod/formatErrors'; // Your error formatter

type Use[FeatureName]Props = {
    initialState?: [FeatureName]State;
    onChange?: (state: [FeatureName]State) => void;
};

export function use[FeatureName]({ initialState, onChange }: Use[FeatureName]Props) {
    // 1. ALWAYS define default state
    const defaultState: [FeatureName]State = {
        // Define your default state structure
    };

    // 2. ALWAYS process initial state with helper function
    function processInitialState(state?: [FeatureName]State): [FeatureName]State | null {
        if (!state) return null;
        // Add any ID generation or parsing logic here
        return stateSchema.parse(state);
    }

    // 3. ALWAYS use reducer with processed initial state
    const [state, dispatch] = useReducer(
        reducer,
        processInitialState(initialState) || defaultState
    );

    // 4. ALWAYS add validation state
    const [isStateValid, setIsStateValid] = useState<boolean | undefined>(undefined);
    const [validationErrors, setValidationErrors] = useState<string[] | undefined>(undefined);

    // 5. ALWAYS validate state on changes
    useEffect(() => {
        const result = validationSchema.safeParse(state);
        if (result.error) {
            setValidationErrors(formatZodErrors(result.error));
        } else {
            setValidationErrors(undefined);
        }
        setIsStateValid(result.success);
    }, [state]);

    // 6. ALWAYS call onChange callback
    useEffect(() => {
        onChange?.(state);
    }, [state, onChange]);

    // 7. ALWAYS create action creators with useCallback
    const add[Entity] = useCallback(([entity]: [EntityType]) => {
        dispatch({ type: 'add-[entity]', payload: { [entity] } });
    }, []);

    const update[Entity] = useCallback((id: string, updates: Partial<[EntityType]>) => {
        dispatch({ type: 'update-[entity]', payload: { id, updates } });
    }, []);

    const remove[Entity] = useCallback((id: string) => {
        dispatch({ type: 'remove-[entity]', payload: { id } });
    }, []);

    // 8. ALWAYS add derived state with useMemo
    const can[Action] = useMemo(() => {
        return isStateValid && /* your conditions */;
    }, [isStateValid, /* dependencies */]);

    // 9. ALWAYS return consistent interface
    return {
        state,
        isStateValid,
        validationErrors,
        add[Entity],
        update[Entity],
        remove[Entity],
        can[Action],
    };
}

Hook Implementation Rules (Must Follow)

  1. Always use 'use client' directive at top
  2. Always define defaultState constant
  3. Always process initialState with helper function
  4. Always include validation with useState + useEffect
  5. Always call onChange in useEffect
  6. Always use useCallback for action creators
  7. Always use useMemo for derived state
  8. Always return consistent interface with state + actions + validation
  9. Import all types from schema - never define inline

Testing Reducer Functions

Why Test Reducers?

Reducer functions are pure functions that take state and actions as input and return new state. This makes them highly testable since they have no side effects and produce predictable outputs.

1. Testing Individual Action Handlers

Test each action handler function in isolation:

import { addItem, updateItem } from "./actions";
import { State } from "./schema";

describe("Action Handlers", () => {
  const initialState: State = {
    items: [
      { id: "1", name: "Item 1", completed: false },
      { id: "2", name: "Item 2", completed: true },
    ],
  };

  describe("addItem", () => {
    it("should add a new item to the state", () => {
      const newItem = { id: "3", name: "Item 3", completed: false };
      const action = { type: "add-item" as const, payload: { item: newItem } };

      const result = addItem(initialState, action);

      expect(result.items).toHaveLength(3);
      expect(result.items[2]).toEqual(newItem);
      expect(result).not.toBe(initialState); // Immutability check
    });

    it("should not modify the original state", () => {
      const newItem = { id: "3", name: "Item 3", completed: false };
      const action = { type: "add-item" as const, payload: { item: newItem } };
      const originalLength = initialState.items.length;

      addItem(initialState, action);

      expect(initialState.items).toHaveLength(originalLength);
    });
  });

  describe("updateItem", () => {
    it("should update the correct item", () => {
      const action = {
        type: "update-item" as const,
        payload: { id: "1", updates: { name: "Updated Item 1" } },
      };

      const result = updateItem(initialState, action);

      expect(result.items[0].name).toBe("Updated Item 1");
      expect(result.items[1]).toEqual(initialState.items[1]); // Other items unchanged
    });

    it("should not modify state if item not found", () => {
      const action = {
        type: "update-item" as const,
        payload: { id: "non-existent", updates: { name: "Updated" } },
      };

      const result = updateItem(initialState, action);

      expect(result.items).toEqual(initialState.items);
    });
  });
});

2. Testing the Main Reducer

Test the reducer's dispatch logic:

import { reducer } from "./reducer";
import { Actions, State } from "./types";

describe("Reducer", () => {
  const initialState: State = {
    items: [{ id: "1", name: "Item 1", completed: false }],
  };

  it("should handle add-item action", () => {
    const action: Actions = {
      type: "add-item",
      payload: { item: { id: "2", name: "Item 2", completed: false } },
    };

    const result = reducer(initialState, action);

    expect(result.items).toHaveLength(2);
  });

  it("should handle update-item action", () => {
    const action: Actions = {
      type: "update-item",
      payload: { id: "1", updates: { completed: true } },
    };

    const result = reducer(initialState, action);

    expect(result.items[0].completed).toBe(true);
  });

  it("should return current state for unknown actions", () => {
    const unknownAction = { type: "unknown-action" } as any;

    const result = reducer(initialState, unknownAction);

    expect(result).toBe(initialState);
  });
});

3. Testing Complex State Updates

For complex nested state updates:

describe("Complex State Updates", () => {
  it("should handle nested group updates correctly", () => {
    const initialState = {
      operator: "AND" as const,
      conditions: [
        {
          id: "group-1",
          type: "group" as const,
          group: {
            operator: "OR" as const,
            rules: [{ id: "rule-1", field: "name", value: "test" }],
          },
        },
      ],
    };

    const action = {
      type: "add-rule-to-group" as const,
      payload: {
        groupId: "group-1",
        rule: { id: "rule-2", field: "email", value: "test@test.com" },
      },
    };

    const result = addRuleToGroup(initialState, action);

    expect(result.conditions[0].group.rules).toHaveLength(2);
    expect(result.conditions[0].group.rules[1].field).toBe("email");
  });
});

4. Testing State Validation

Test validation logic within your hooks:

import { renderHook } from "@testing-library/react";
import { useFeature } from "./index";

describe("useFeature validation", () => {
  it("should mark state as invalid when required fields are missing", () => {
    const { result } = renderHook(() =>
      useFeature({
        initialState: { items: [] }, // Missing required fields
      })
    );

    expect(result.current.isStateValid).toBe(false);
    expect(result.current.validationErrors).toBeDefined();
  });

  it("should mark state as valid when all required fields are present", () => {
    const validState = {
      items: [{ id: "1", name: "Item 1", completed: false }],
      metadata: { version: "1.0" },
    };

    const { result } = renderHook(() =>
      useFeature({
        initialState: validState,
      })
    );

    expect(result.current.isStateValid).toBe(true);
    expect(result.current.validationErrors).toBeUndefined();
  });
});

5. Testing Edge Cases

Always test boundary conditions and edge cases:

describe("Edge Cases", () => {
  it("should handle empty state gracefully", () => {
    const emptyState = { items: [] };
    const action = { type: "update-item", payload: { id: "1", updates: {} } };

    const result = updateItem(emptyState, action);

    expect(result.items).toEqual([]);
  });

  it("should handle malformed action payloads", () => {
    const state = { items: [{ id: "1", name: "Item" }] };
    const malformedAction = { type: "update-item", payload: {} } as any;

    expect(() => updateItem(state, malformedAction)).not.toThrow();
  });
});

6. Test Organization Tips

  • Group by Feature: Organize tests by action type or feature area
  • Use Descriptive Names: Test names should clearly describe what they're testing
  • Test Both Success and Failure Cases: Include edge cases and error conditions
  • Verify Immutability: Always check that original state isn't modified
  • Use Test Data Builders: Create helper functions for complex test data

7. Jest Test Utilities

Create reusable test utilities:

// test-utils.ts
export function createMockState(overrides: Partial<State> = {}): State {
  return {
    items: [],
    metadata: { version: "1.0" },
    ...overrides,
  };
}

export function createMockAction<T extends Actions["type"]>(
  type: T,
  payload?: Extract<Actions, { type: T }>["payload"]
): Extract<Actions, { type: T }> {
  return { type, payload } as Extract<Actions, { type: T }>;
}

Code Templates for LLM Reference

Complete Working Example

// schema.ts
import { z } from "zod";

export const validationSchema = z.object({
  items: z.array(
    z.object({
      id: z.string(),
      name: z.string(),
      completed: z.boolean(),
    })
  ),
  metadata: z.object({
    version: z.string(),
  }),
});

export const stateSchema = z.object({
  items: z
    .array(
      z.object({
        id: z.string(),
        name: z.string().nullish(),
        completed: z.boolean().nullish(),
      })
    )
    .nullish(),
  metadata: z
    .object({
      version: z.string().nullish(),
    })
    .nullish(),
});

export type TodoState = z.infer<typeof stateSchema>;
export type TodoItem = z.infer<typeof stateSchema>["items"][0];
// actions.ts
import { TodoState, TodoItem } from "./schema";

export type AddItemAction = {
  type: "add-item";
  payload: { item: TodoItem };
};

export type UpdateItemAction = {
  type: "update-item";
  payload: { id: string; updates: Partial<TodoItem> };
};

export type RemoveItemAction = {
  type: "remove-item";
  payload: { id: string };
};

export type Actions = AddItemAction | UpdateItemAction | RemoveItemAction;

export function addItem(state: TodoState, action: AddItemAction): TodoState {
  return {
    ...state,
    items: [...(state.items || []), action.payload.item],
  };
}

export function updateItem(
  state: TodoState,
  action: UpdateItemAction
): TodoState {
  return {
    ...state,
    items: (state.items || []).map((item) =>
      item.id === action.payload.id
        ? { ...item, ...action.payload.updates }
        : item
    ),
  };
}

export function removeItem(
  state: TodoState,
  action: RemoveItemAction
): TodoState {
  return {
    ...state,
    items: (state.items || []).filter((item) => item.id !== action.payload.id),
  };
}

Rules Summary for LLMs

MANDATORY Requirements

  1. File Structure: index.ts, actions.ts, reducer.ts, schema.ts
  2. Schema Pattern: validationSchema + stateSchema (dual approach)
  3. Action Pattern: Discriminated union + separate handler functions
  4. Reducer Pattern: Switch statement only, import all handlers
  5. Hook Pattern: useReducer + validation + useCallback actions
  6. Testing: Test each action handler function individually
  7. Naming: Consistent conventions across all files

FORBIDDEN Patterns

  1. ❌ Logic inline in reducer switch cases
  2. ❌ Mutating state objects directly
  3. ❌ Missing default case in reducer
  4. ❌ Action creators without useCallback
  5. ❌ Missing validation setup
  6. ❌ Inconsistent naming conventions
  7. ❌ Side effects in action handlers

Quick Validation Checklist

  • All files follow exact template structure
  • Both validationSchema and stateSchema defined
  • All action types use discriminated unions
  • All action handlers are pure functions
  • Reducer only contains switch statement
  • Hook includes validation state management
  • All action creators use useCallback
  • Tests cover each action handler

LLM Implementation Instructions

When implementing a useReducer hook, follow these exact steps:

Step 1: Create schema.ts

// Copy this template and replace [FeatureName] and field definitions
import { z } from 'zod';

export const validationSchema = z.object({
  // Define strict required schema here
});

export const stateSchema = z.object({
  // Define permissive nullish schema here
});

export type [FeatureName]State = z.infer<typeof stateSchema>;

Step 2: Create actions.ts

// Follow the exact action pattern with discriminated unions
import { [FeatureName]State } from './schema';

export type [Action]Action = {
  type: '[action-name]';
  payload: { /* action data */ };
};

export type Actions = [Action1]Action | [Action2]Action;

export function [actionHandler](state: [FeatureName]State, action: [Action]Action): [FeatureName]State {
  // Return new state object with immutable updates
}

Step 3: Create reducer.ts

// Switch statement only - import all handlers
import { [FeatureName]State } from './schema';
import { Actions, [handler1], [handler2] } from './actions';

export function reducer(state: [FeatureName]State, action: Actions): [FeatureName]State {
  switch (action.type) {
    case '[action-type]':
      return [handler](state, action);
    default:
      return state;
  }
}

Step 4: Create index.ts

// Follow the complete hook template with validation
"use client";
// Import all dependencies and follow the 9-step template

Step 5: Create tests

// Test each action handler function individually
// Verify immutability and state correctness

Common Integration Patterns

With React Query

const saveMutation = useMutation({
  mutationFn: (data) => {
    const result = validationSchema.safeParse(state);
    if (!result.success) throw new Error("Invalid state");
    return apiCall(result.data);
  },
});

With Notifications

const { sendSuccess, sendError } = useNotifications();

const handleSave = () => {
  const result = validationSchema.safeParse(state);
  if (!result.success) {
    sendError("Please fix validation errors before saving");
    return;
  }

  try {
    // Save logic here
    sendSuccess("Changes saved successfully");
  } catch (error) {
    sendError("Failed to save changes");
  }
};

Remember: Always follow the templates exactly. These patterns ensure consistency, type safety, and maintainability across all useReducer implementations.

Technology Stack & Tooling

For LLM Reference: This document defines the standard technology stack and tooling decisions for all projects. Follow these choices consistently unless there's a compelling reason to deviate.

Core Technologies

Primary Language

TypeScript is the primary language for all development.

Justification:

  • Fast Development: Rich IDE support, auto-completion, and refactoring tools
  • Large Community: Extensive ecosystem and community support
  • Team Navigation: Makes it easier for team members to understand and navigate codebases

Web Development Stack

Backend Services

  • Hono: Primary framework for backend services
  • Cloud Run: Preferred deployment target for containerized services
  • Avoid serverless for services (use containers instead)

Frontend Applications

  • React: Core UI library
  • Next.js: React framework for web applications
  • TailwindCSS: Utility-first CSS framework

Authentication

  • Clerk: Primary authentication provider

Database

  • Neon: PostgreSQL database provider

Cloud Platform

  • Google Cloud Platform (GCP): Primary cloud infrastructure

Development Tools

  • GitHub: Source code management
  • GitHub Actions: CI/CD pipelines
  • Prettier: Code formatting (mandatory)

Monorepo Structure

Turbo Monorepo Layout

/
├── apps/           # Applications (deployable units)
│   ├── web/        # Web applications
│   └── api/        # API services
└── packages/       # Shared packages
    ├── ui/         # Shared UI components
    ├── utils/      # Shared utilities
    └── config/     # Shared configuration

Application Types

  • Web Apps: React/Next.js applications
  • Services: Hono-based API services deployed to Cloud Run

NextJS Project Structure

Standard Directory Layout

/apps/web/
├── app/                 # Next.js App Router
├── components/       # Shared components
├── features/         # Feature-based modules
│   └── [feature]/
│       ├── actions/         # Server actions
│       ├── components/
│       ├── data-providers/       # Context providers
│       ├── functions/       # Pure utility functions
│       ├── hooks/           # Custom React hooks
│       └── types/
├── lib/ # abstractions of used libraries
├── utilities/       # Application utilities
└── public/             # Static assets

Package Guidelines

  • Stateless: Packages should be stateless and reusable
  • Single Responsibility: Each package should have a clear purpose
  • Minimal Dependencies: Keep dependencies minimal and well-justified

Deployment Preferences

Service Deployment

  • Cloud Run: Preferred for containerized services
  • Containers over Serverless: Better control and debugging capabilities
  • GitHub Actions: For CI/CD pipelines

Database

  • Neon PostgreSQL: Primary database choice
  • Schema migrations: Use database migration tools
  • Connection pooling: Implement proper connection management

Development Environment

Required Tools

  • Prettier: Code formatting (non-negotiable)
  • TypeScript: Strong typing throughout
  • ESLint: Code linting and quality
  • Git: Version control with proper branching strategy

IDE Requirements

  • TypeScript support: Full IntelliSense and type checking
  • Prettier integration: Automatic formatting on save
  • ESLint integration: Real-time linting feedback

Comments are disabled for this gist.