JavaScript's module system and TypeScript's type system create unique opportunities for proximity, especially in component-based architectures.
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: 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;// 🧠 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" };// 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);
};// 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);
};// 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;
}
}// 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 };
};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: 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: 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()}
/>
);
};{
"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"
}
}// 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;// 🧠 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>
);
};- 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?
See javascript-examples/ for complete examples including:
- React app with full component colocation
- Node.js API with proximity patterns
- TypeScript library with decision archaeology