Skip to content
This repository was archived by the owner on Jun 14, 2026. It is now read-only.

Latest commit

 

History

History
447 lines (364 loc) · 12.1 KB

File metadata and controls

447 lines (364 loc) · 12.1 KB

JavaScript/TypeScript Proximity Patterns

🎯 JavaScript/TypeScript-Specific Considerations

JavaScript's module system and TypeScript's type system create unique opportunities for proximity, especially in component-based architectures.

📦 Module Organization

Proximity-First Module Structure

Traditional (Anti-Pattern):

src/
├── components/
│   └── UserCard.tsx
├── hooks/
│   └── useUser.ts
├── utils/
│   └── validation.ts
├── types/
│   └── user.ts
└── tests/
    └── UserCard.test.tsx

Proximity Pattern:

src/
├── features/
│   └── user/
│       ├── UserCard.tsx           # Component
│       ├── UserCard.test.tsx      # Test
│       ├── UserCard.stories.tsx   # Storybook
│       ├── UserCard.styles.ts     # Styles
│       ├── useUser.ts             # Custom hook
│       ├── user.types.ts         # Types
│       ├── user.utils.ts         # Utilities
│       └── user.constants.ts     # Constants

🧠 Decision Documentation in JavaScript/TypeScript

Constants with Context

// 🧠 DECISION: 5-second debounce for search input
// Why: Reduces API calls while maintaining responsive feel
// Measured: 5s reduces calls by 70% vs no debounce
// Alternative: 1s debounce - still too many API calls
export const SEARCH_DEBOUNCE_MS = 5000;

// 🛡️ SECURITY: 10MB max file upload size
// Threat: DoS through large file uploads
// Calculation: 99% of legitimate files < 5MB
// Buffer: 2x for edge cases = 10MB
export const MAX_UPLOAD_SIZE_BYTES = 10 * 1024 * 1024;

// ⚡ PERFORMANCE: Virtual list threshold at 100 items
// Benchmark: Rendering 100 items = 16ms, 1000 items = 160ms
// Target: Keep under 1 frame (16.67ms) for 60fps
export const VIRTUAL_LIST_THRESHOLD = 100;

Type Decisions

// 🧠 DECISION: Using branded types for IDs
// Why: Prevent mixing different ID types at compile time
// Example: Can't pass UserId where PostId expected
type Brand<K, T> = K & { __brand: T };

export type UserId = Brand<string, "UserId">;
export type PostId = Brand<string, "PostId">;

// 🧠 DECISION: Discriminated union for API responses
// Why: Enables exhaustive pattern matching
// Alternative: Optional error field - loses type safety
export type ApiResponse<T> = 
  | { status: "success"; data: T }
  | { status: "error"; error: string }
  | { status: "loading" };

⚾ Three-Strikes in JavaScript

Evolution of Utilities

// Strike 1: In UserForm.jsx
const validateEmail = (email) => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

// Strike 2: In NewsletterForm.jsx
const validateEmail = (email) => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

// Strike 3: Extract to utils/validation.js
// 🔧 EXTRACTION: Email validation
// Used in: UserForm, NewsletterForm, ContactForm
export const validateEmail = (email) => {
  // 🧠 DECISION: Simple regex over complex library
  // Why: Covers 99% of cases, 0 dependencies
  // Note: Not RFC 5322 compliant, but good enough
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

🎯 Component Colocation

Everything Component-Related Together

// UserCard.tsx - Component file
import { UserCardProps } from "./UserCard.types";
import { useUserCard } from "./useUserCard";
import * as S from "./UserCard.styles";

export const UserCard: React.FC<UserCardProps> = ({ userId }) => {
  // 🧠 DECISION: Fetch data in component, not parent
  // Why: Component owns its data requirements
  // Alternative: Prop drilling - creates coupling
  const { user, loading, error } = useUserCard(userId);
  
  if (loading) return <S.Skeleton />;
  if (error) return <S.Error>{error.message}</S.Error>;
  
  return (
    <S.Card>
      <S.Avatar src={user.avatar} />
      <S.Name>{user.name}</S.Name>
    </S.Card>
  );
};

// UserCard.test.tsx - Test file
describe("UserCard", () => {
  it("displays user name", () => {
    // Test implementation
  });
});

// UserCard.styles.ts - Styles
import styled from "styled-components";

// 🧠 DECISION: Styled-components over CSS modules
// Why: Component-scoped styles with theme support
// Tradeoff: 15KB bundle size for runtime CSS
export const Card = styled.div`
  padding: ${({ theme }) => theme.spacing.md};
`;

// useUserCard.ts - Custom hook
export const useUserCard = (userId: string) => {
  // 🧠 DECISION: SWR for data fetching
  // Why: Built-in cache, revalidation, error handling
  // Alternative: React Query - similar, team prefers SWR
  return useSWR(`/api/users/${userId}`, fetcher);
};

🔒 Error Handling Proximity

Error Boundaries Near Components

// In UserProfile/ErrorBoundary.tsx - specific to UserProfile

class UserProfileErrorBoundary extends Component {
  // 🧠 DECISION: Component-specific error boundary
  // Why: User profile errors shouldn't crash entire app
  // Recovery: Show fallback UI with retry button
  
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error: Error) {
    // 🛡️ SECURITY: Sanitize error messages
    // Why: Stack traces might leak sensitive info
    const sanitized = sanitizeError(error);
    return { hasError: true, error: sanitized };
  }
  
  componentDidCatch(error: Error, info: ErrorInfo) {
    // 📊 MONITORING: Send to error tracking
    // Service: Sentry with user context
    trackError(error, {
      component: "UserProfile",
      ...info
    });
  }
  
  render() {
    if (this.state.hasError) {
      return <UserProfileFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}

🏗️ Hook Proximity

Custom Hooks Near Usage

// In TodoList/useTodos.ts - not in global hooks/

// 🧠 DECISION: Local custom hook vs global
// Why: Todo-specific logic shouldn't be globally available
// Benefit: Can evolve with TodoList without affecting others

export const useTodos = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  
  // ⚡ PERFORMANCE: Optimistic updates
  // Why: Instant feedback improves perceived performance
  // Rollback: On error, revert and show toast
  const addTodo = useCallback(async (text: string) => {
    const tempId = Date.now();
    const newTodo = { id: tempId, text, completed: false };
    
    // Optimistically add
    setTodos(prev => [...prev, newTodo]);
    
    try {
      const saved = await api.createTodo(text);
      // Replace temp with real
      setTodos(prev => 
        prev.map(t => t.id === tempId ? saved : t)
      );
    } catch (error) {
      // Rollback on error
      setTodos(prev => prev.filter(t => t.id !== tempId));
      toast.error("Failed to add todo");
    }
  }, []);
  
  return { todos, addTodo };
};

🎭 Event Handler Proximity

Handlers Near Components

const SearchBar: React.FC = () => {
  // 🧠 DECISION: Inline handlers for simple logic
  // Why: Keeps component self-contained
  // Threshold: Extract if > 5 lines
  
  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault();
    // Simple logic stays inline
    const query = e.currentTarget.search.value;
    router.push(`/search?q=${query}`);
  };
  
  // ⚡ PERFORMANCE: Debounced autocomplete
  // Why: Reduce API calls during typing
  // Delay: 300ms feels responsive, saves 80% of calls
  const handleAutocomplete = useMemo(
    () => debounce((value: string) => {
      if (value.length > 2) {
        fetchSuggestions(value);
      }
    }, 300),
    []
  );
  
  return (
    <form onSubmit={handleSearch}>
      <input 
        name="search"
        onChange={(e) => handleAutocomplete(e.target.value)}
      />
    </form>
  );
};

⚡ Performance Patterns

Optimization Documentation

// ⚡ PERFORMANCE: Memoized expensive computation
// Benchmark: Reduces re-renders from 100ms to 1ms
// Dependencies: Only recalculate when items change
const expensiveValue = useMemo(() => {
  // 🧠 DECISION: Calculate client-side vs API call
  // Why: Faster for < 1000 items
  // Threshold: Use API for > 1000 items
  if (items.length > 1000) {
    return fetchCalculation(items);
  }
  return calculateLocally(items);
}, [items]);

// ⚡ PERFORMANCE: Lazy load heavy component
// Size: 250KB gzipped
// Strategy: Load when user scrolls near
const HeavyChart = lazy(() => 
  import(
    /* webpackChunkName: "heavy-chart" */
    /* webpackPrefetch: true */
    "./HeavyChart"
  )
);

🛡️ Security Proximity

Security at Component Level

// 🛡️ SECURITY: XSS prevention
// Threat: User-generated HTML content
// Mitigation: DOMPurify with strict whitelist

const UserComment: React.FC<{ html: string }> = ({ html }) => {
  const sanitized = useMemo(() => {
    // 🧠 DECISION: Whitelist approach over blacklist
    // Why: Safer to explicitly allow than deny
    return DOMPurify.sanitize(html, {
      ALLOWED_TAGS: ["b", "i", "em", "strong", "p"],
      ALLOWED_ATTR: []
    });
  }, [html]);
  
  return (
    <div 
      dangerouslySetInnerHTML={{ __html: sanitized }}
      // 🛡️ SECURITY: CSP nonce for inline styles
      nonce={getCSPNonce()}
    />
  );
};

📏 Package.json Proximity

Dependency Documentation

{
  "dependencies": {
    // 🧠 DECISION: React 18 for concurrent features
    // Why: Automatic batching improves performance
    // Migration: From v17, tested all components
    "react": "^18.2.0",
    
    // 🛡️ SECURITY: Latest axios for security patches
    // CVE: Addresses prototype pollution vulnerability
    "axios": "^1.6.0",
    
    // ⚡ PERFORMANCE: Preact for production build
    // Size: React = 45KB, Preact = 3KB gzipped
    // Tradeoff: Some React features unavailable
    "preact": "^10.19.0"
  },
  "devDependencies": {
    // 🧠 DECISION: Vite over Webpack
    // Why: 10x faster HMR, better DX
    // Benchmark: Webpack = 30s, Vite = 3s cold start
    "vite": "^5.0.0"
  }
}

🔧 Configuration Proximity

Config Near Features

// In features/payment/config.ts - not in global config

// 🧠 DECISION: Feature-specific configuration
// Why: Payment config has no relevance elsewhere
export const PAYMENT_CONFIG = {
  // 🛡️ SECURITY: PCI compliance settings
  // Requirement: Never store card numbers
  tokenizeCards: true,
  
  // ⚡ PERFORMANCE: Retry configuration
  // Pattern: Exponential backoff with jitter
  retries: {
    max: 3,
    initialDelay: 1000,
    multiplier: 2,
    jitter: true
  },
  
  // 🧠 DECISION: Supported payment methods
  // Why: Start with cards, add others based on usage
  methods: ["card", "apple_pay", "google_pay"]
} as const;

🎯 State Management Proximity

Local State Over Global

// 🧠 DECISION: Component state over global store
// Why: State only relevant to this component tree
// Rule: Lift state only when sibling needs it

const TodoApp = () => {
  // Local state for todos
  const [todos, setTodos] = useState<Todo[]>([]);
  
  // 🧠 DECISION: Derived state over stored
  // Why: Single source of truth
  const completedCount = todos.filter(t => t.completed).length;
  const activeCount = todos.length - completedCount;
  
  // Only lift to context if multiple components need it
  return (
    <TodoContext.Provider value={{ todos, setTodos }}>
      <TodoList />
      <TodoStats count={activeCount} />
    </TodoContext.Provider>
  );
};

🔍 JavaScript/TypeScript Audit Checklist

  • Are components colocated with tests, styles, and hooks?
  • Do constants include decision documentation?
  • Are error boundaries component-specific?
  • Do performance optimizations include measurements?
  • Are security measures documented at point of use?
  • Is state kept local until needed elsewhere?
  • Are types defined near their usage?
  • Do package.json entries explain choices?
  • Are handlers defined in components?
  • Does code follow the 3-strikes rule?

📚 Examples

See javascript-examples/ for complete examples including:

  • React app with full component colocation
  • Node.js API with proximity patterns
  • TypeScript library with decision archaeology