Coding Standards and Best Practices
Introduction
This document outlines our coding standards and best practices. Following these guidelines ensures we create maintainable, robust, and high-quality applications.
Why These Standards Matter
Consistent coding standards directly impact our ability to build reliable software at scale:
- Reduced cognitive load: Engineers understand code faster when it follows familiar patterns
- Improved collaboration: Shared conventions facilitate better teamwork and code reviews
- Higher code quality: Battle-tested patterns help us avoid common pitfalls
- Sustainable development: Well-structured code remains maintainable as it evolves
- Faster onboarding: New team members become productive more quickly
- Clear semantic intent: Well-named functions, variables, and components communicate their purpose clearly
When we prioritize semantic clarity, we write code that reveals its intent to human readers. Meaningful naming and logical organization create a codebase that tells a coherent story about our product.
Key Principles at a Glance
Architecture & Design
- SOLID: Single responsibility, Open-closed, Liskov substitution, Interface segregation, Dependency inversion
- CLEAN: Cohesive, Loosely coupled, Encapsulated, Assertive, Non-redundant
- Composition over Inheritance: Build systems from small, focused parts
- Functional First: Favor pure functions, immutability, and data transformations
- Separation of Concerns: Keep distinct aspects of your application isolated
Code Quality
- DRY: Don't repeat yourself—extract reusable patterns
- KISS: Keep it simple—avoid unnecessary complexity
- YAGNI: You aren't gonna need it—build for current requirements only
- Fail Fast: Make problems visible immediately
- Law of Demeter: Minimize dependencies between components
TypeScript & React
- Types over Interfaces: Use types for most cases, interfaces when extending
- Container/Presentational Pattern: Separate data handling from UI rendering
- Custom Hooks: Extract reusable stateful logic
- Context for State: Use React Context for shared state, Redux for complex state
Core Principles
SOLID Principles
These principles form the foundation of maintainable software design:
Single Responsibility Principle
Each function, component, or module should do exactly one thing.
// Bad: Component handling multiple concerns
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
// Good: Separated concerns
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
return { user, loading };
}
function UserProfile({ userId }) {
const { user, loading } = useUser(userId);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
Open/Closed Principle
Code should be open for extension but closed for modification.
// Bad: Will need modification for each new shape
function calculateArea(shape) {
if (shape.type === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
}
// Adding a new shape requires changing this function
}
// Good: Open for extension without modification
const shapeCalculators = {
circle: (shape) => Math.PI * shape.radius ** 2,
rectangle: (shape) => shape.width * shape.height,
// New shapes can be added without changing existing code
triangle: (shape) => (shape.base * shape.height) / 2
};
function calculateArea(shape) {
const calculator = shapeCalculators[shape.type];
if (!calculator) {
throw new Error(`Unsupported shape type: ${shape.type}`);
}
return calculator(shape);
}
Dependency Inversion Principle
Depend on abstractions, not concrete implementations.
// Bad: Direct dependency on implementation
function UserService() {
this.repository = new MySQLUserRepository();
this.getUser = (id) => {
return this.repository.findById(id);
};
}
// Good: Dependency injection
function createUserService(repository) {
return {
getUser: (id) => repository.findById(id)
};
}
// Usage
const userRepo = createUserRepository();
const userService = createUserService(userRepo);
KISS (Keep It Simple, Stupid)
Simpler solutions are easier to understand, maintain, and debug.
// Overcomplicated
function getActiveUsers(users) {
const activeUsers = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (user.status === 'active' &&
user.lastLogin &&
(new Date() - new Date(user.lastLogin)) / (1000 * 60 * 60 * 24) < 30) {
activeUsers.push(user);
}
}
return activeUsers;
}
// Simpler and clearer
function isRecentlyActive(user) {
if (!user.lastLogin) return false;
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
return user.status === 'active' && new Date(user.lastLogin) > thirtyDaysAgo;
}
function getActiveUsers(users) {
return users.filter(isRecentlyActive);
}
DRY (Don't Repeat Yourself)
Extract repeated code into reusable functions.
// Wet code with repetition
function validateUserForm(data) {
if (!data.name) {
return { valid: false, error: 'Name is required' };
}
if (!data.email) {
return { valid: false, error: 'Email is required' };
}
if (!data.email.includes('@')) {
return { valid: false, error: 'Email is invalid' };
}
return { valid: true };
}
// DRY code with abstraction
function required(value, fieldName) {
if (!value) return `${fieldName} is required`;
return null;
}
function validEmail(value) {
if (!value.includes('@')) return 'Email is invalid';
return null;
}
function validateUserForm(data) {
const validators = {
name: [value => required(value, 'Name')],
email: [value => required(value, 'Email'), validEmail]
};
for (const [field, fieldValidators] of Object.entries(validators)) {
for (const validate of fieldValidators) {
const error = validate(data[field]);
if (error) return { valid: false, error };
}
}
return { valid: true };
}
YAGNI (You Aren't Gonna Need It)
Build only what you need now, not what you might need later.
// Overengineered: Building for speculative requirements
function createUser(userData) {
return {
id: generateId(),
name: userData.name,
email: userData.email,
createdAt: new Date(),
metadata: {}, // "Just in case we need this later"
preferences: {}, // "We might add preferences in the future"
roles: ['user'], // "We might add role management later"
};
}
// YAGNI: Build only what you need now
function createUser(userData) {
return {
id: generateId(),
name: userData.name,
email: userData.email,
createdAt: new Date()
};
}
Fail Fast
Detect and report errors as soon as possible.
// Bad: Silent failure
function transferMoney(fromAccount, toAccount, amount) {
if (fromAccount.balance >= amount) {
fromAccount.balance -= amount;
toAccount.balance += amount;
return true;
}
return false; // Silently fails
}
// Good: Fail fast with clear errors
function transferMoney(fromAccount, toAccount, amount) {
if (amount <= 0) {
throw new Error('Transfer amount must be positive');
}
if (fromAccount.balance < amount) {
throw new Error(`Insufficient funds: needed ${amount}, had ${fromAccount.balance}`);
}
fromAccount.balance -= amount;
toAccount.balance += amount;
return {
success: true,
fromBalance: fromAccount.balance,
toBalance: toAccount.balance
};
}
Architecture Patterns
Composition Over Inheritance
Build systems from small, focused parts rather than complex hierarchies.
// Bad: Deep inheritance hierarchy
class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class ElectricCar extends Car { /* ... */ }
class ElectricSUV extends ElectricCar { /* ... */ }
// Good: Composition of behaviors
const createVehicle = (options) => ({
speed: 0,
position: { x: 0, y: 0 },
move() { this.position.x += this.speed; }
});
const withEngine = (vehicle) => ({
...vehicle,
startEngine() { console.log('Engine started'); },
stopEngine() { console.log('Engine stopped'); }
});
const withElectric = (vehicle) => ({
...vehicle,
charge: 100,
chargeBattery() { this.charge = 100; }
});
// Create a vehicle by composing behaviors
const createElectricCar = () => {
const base = createVehicle();
return withElectric(withEngine(base));
};
Functional Programming Principles
Prefer pure functions, immutability, and declarative patterns.
// Imperative with mutations
function processOrders(orders) {
let total = 0;
const processed = [];
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === 'completed') {
total += orders[i].amount;
processed.push({
...orders[i],
processed: true
});
}
}
return { total, processed };
}
// Functional with pure functions and immutability
function processOrders(orders) {
const processed = orders
.filter(order => order.status === 'completed')
.map(order => ({ ...order, processed: true }));
const total = processed
.reduce((sum, order) => sum + order.amount, 0);
return { total, processed };
}
Separation of Concerns
Keep distinct aspects of your application logically isolated.
// Bad: Mixed concerns
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
// Form validation mixed with API calls and UI updates
const values = getFormValues(event.target);
if (!values.email.includes('@')) {
setError('Invalid email');
setIsLoading(false);
return;
}
try {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(values)
});
const data = await response.json();
if (!response.ok) {
setError(data.message);
setIsLoading(false);
return;
}
router.push(`/users/${data.id}`);
} catch (err) {
setError('Network error');
setIsLoading(false);
}
}
// Good: Separated concerns
// 1. Validation logic
function validateUser(data) {
const errors = {};
if (!data.email?.includes('@')) errors.email = 'Invalid email';
if (!data.name) errors.name = 'Name is required';
return {
isValid: Object.keys(errors).length === 0,
errors
};
}
// 2. API interaction
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to create user');
}
return data;
}
// 3. UI component with clear responsibilities
function UserForm() {
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({});
const router = useRouter();
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
setErrors({});
const values = getFormValues(event.target);
const validation = validateUser(values);
if (!validation.isValid) {
setErrors(validation.errors);
setIsLoading(false);
return;
}
try {
const user = await createUser(values);
router.push(`/users/${user.id}`);
} catch (err) {
setErrors({ form: err.message });
setIsLoading(false);
}
}
// Render form...
}
JavaScript/TypeScript Guidelines
Type System Usage
Prefer Types Over Interfaces for most use cases.
// Prefer type for general object shapes
type User = {
id: string;
name: string;
email: string;
};
// Prefer type for unions
type Status = 'pending' | 'active' | 'inactive';
// Use interface when extending is necessary
interface BaseComponent {
render(): JSX.Element;
}
interface FormComponent extends BaseComponent {
validate(): boolean;
}
// Use discriminated unions for state modeling
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// Usage example
function UserProfile() {
const [userState, setUserState] = useState<RequestState<User>>({ status: 'idle' });
useEffect(() => {
setUserState({ status: 'loading' });
fetchUser()
.then(data => setUserState({ status: 'success', data }))
.catch(error => setUserState({ status: 'error', error }));
}, []);
// Safe handling of all possible states
switch (userState.status) {
case 'idle':
return <div>Press button to load user</div>;
case 'loading':
return <div>Loading...</div>;
case 'error':
return <div>Error: {userState.error.message}</div>;
case 'success':
return <div>Hello, {userState.data.name}</div>;
}
}
Error Handling
Use structured error types and meaningful messages.
// Define error types with discriminated unions
type NetworkError = {
kind: 'network';
message: string;
originalError: Error;
};
type ValidationError = {
kind: 'validation';
message: string;
field?: string;
};
type AppError = NetworkError | ValidationError;
// Create error factories instead of classes
const createNetworkError = (originalError: Error): NetworkError => ({
kind: 'network',
message: `Network failure: ${originalError.message}`,
originalError
});
const createValidationError = (message: string, field?: string): ValidationError => ({
kind: 'validation',
message,
field
});
// Handle errors safely with type narrowing
function handleError(error: AppError) {
if (error.kind === 'network') {
// Handle network errors
console.error(`Network error: ${error.message}`);
return `Please check your connection and try again`;
} else {
// Handle validation errors
return error.field
? `${error.field}: ${error.message}`
: error.message;
}
}
// Use try/catch with proper error transformation
async function fetchData(url: string) {
try {
const response = await fetch(url);
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw createValidationError(
data.message || `Server error: ${response.status}`,
data.field
);
}
return await response.json();
} catch (error) {
// Transform unknown errors to our error type
if (error instanceof Error && !(error as any).kind) {
throw createNetworkError(error);
}
throw error;
}
}
Asynchronous Code
Use async/await with proper error handling.
// Prefer async/await over promise chains
async function loadUserData(userId) {
try {
const user = await fetchUser(userId);
const permissions = await fetchPermissions(user.id);
return {
...user,
permissions
};
} catch (error) {
// Handle errors appropriately
console.error('Failed to load user data:', error);
throw error;
}
}
// Parallel operations with Promise.all
async function loadDashboardData(userId) {
try {
const [user, notifications, stats] = await Promise.all([
fetchUser(userId),
fetchNotifications(userId),
fetchUserStats(userId)
]);
return { user, notifications, stats };
} catch (error) {
console.error('Failed to load dashboard:', error);
throw error;
}
}
React Development
Component Architecture
We follow a strict separation between presentational components and business logic:
Component/Hook Pattern
Use the <component-name>.tsx and use-<component-name>.ts pattern to separate concerns:
Presentation Component
// user-profile.tsx - Presentation-focused component
import { useUserProfile } from './use-user-profile';
import { Avatar } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card';
type UserProfileProps = {
userId: string;
showEmail?: boolean;
};
export function UserProfile({ userId, showEmail = false }: UserProfileProps) {
// Hook handles all business logic and data fetching
const { user, loading, error, sendMessage } = useUserProfile(userId);
if (loading) {
return <div className="loading">Loading user profile...</div>;
}
if (error) {
return <div className="error">Error: {error.message}</div>;
}
if (!user) {
return <div className="error">User not found</div>;
}
return (
<Card className="user-profile">
<CardHeader>
<Avatar
src={user.avatarUrl}
alt={user.name}
fallback={user.name.charAt(0)}
/>
<h2>{user.name}</h2>
</CardHeader>
<CardContent>
{showEmail && <p>{user.email}</p>}
</CardContent>
<CardFooter>
<Button onClick={sendMessage}>
Send Message
</Button>
</CardFooter>
</Card>
);
}
Logic Hook
// use-user-profile.ts - All business logic lives here
import { useState, useEffect } from 'react';
import { fetchUserData } from '@/lib/api/users';
export type User = {
id: string;
name: string;
email: string;
avatarUrl?: string;
};
export function useUserProfile(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let isMounted = true;
async function loadUser() {
try {
setLoading(true);
const data = await fetchUserData(userId);
if (isMounted) {
setUser(data);
setLoading(false);
}
} catch (error) {
if (isMounted) {
setError(error instanceof Error ? error : new Error('Unknown error'));
setLoading(false);
}
}
}
loadUser();
return () => {
isMounted = false;
};
}, [userId]);
const sendMessage = () => {
// Message sending implementation
console.log(`Sending message to ${userId}`);
};
return {
user,
loading,
error,
sendMessage
};
}
Component Naming and File Organization
Follow these naming conventions for consistency:
| Type | Convention | Example |
|---|---|---|
| File naming | Use kebab-case for all files | user-profile.tsx, use-user-profile.ts |
| Component naming | Use PascalCase for component names | UserProfile |
| Hook naming | Use camelCase with use prefix | useUserProfile |
| Directory structure | Group related components in feature directories | See example below |
Example directory structure:
src/
├── components/
│ ├── ui/ # ShadCN components
│ ├── user/
│ │ ├── user-profile.tsx
│ │ ├── use-user-profile.ts
│ │ ├── user-avatar.tsx
│ │ └── user-settings/
│ │ ├── user-settings.tsx
│ │ └── use-user-settings.ts
│ └── dashboard/
│ ├── dashboard.tsx
│ ├── use-dashboard.ts
│ └── dashboard-widgets/
└── lib/
├── api/
├── utils/
└── hooks/ # Shared hooks
ShadCN UI Components
Use ShadCN UI components whenever possible for consistency and accessibility:
// Bad: Custom unstyled elements
function LoginForm() {
return (
<div className="form-container">
<div className="form-group">
<label>Email</label>
<input type="email" />
</div>
<div className="form-group">
<label>Password</label>
<input type="password" />
</div>
<button>Log In</button>
</div>
);
}
// Good: ShadCN components
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
function LoginForm() {
return (
<Card>
<CardHeader>
<CardTitle>Login</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
</div>
</CardContent>
<CardFooter>
<Button>Log In</Button>
</CardFooter>
</Card>
);
}
Benefits of using ShadCN
- Consistent design language across the application
- Built-in accessibility features
- Responsive by default
- Customizable through Tailwind
- Reduced development time
Custom Hooks
Extract reusable logic into custom hooks:
// Generic data fetching hook
function useData<T>(url: string, options?: RequestInit) {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({
data: null,
loading: true,
error: null
});
useEffect(() => {
let isMounted = true;
async function fetchData() {
setState(prev => ({ ...prev, loading: true }));
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
if (isMounted) {
setState({ data, loading: false, error: null });
}
} catch (error) {
if (isMounted) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error')
});
}
}
}
fetchData();
return () => {
isMounted = false;
};
}, [url, JSON.stringify(options)]);
return state;
}
// Usage with our naming conventions
// user-list.tsx
function UserList() {
const { users, loading, error } = useUserList();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!users?.length) return <div>No users found</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// use-user-list.ts
function useUserList() {
const { data, loading, error } = useData<User[]>('/api/users');
return {
users: data,
loading,
error
};
}
State Management
Local vs. Global State
Default to local state whenever possible and only elevate to global state when necessary
// Local component state (default approach)
// In use-counter.ts
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// In counter.tsx
function Counter() {
const { count, increment } = useCounter();
return (
<div>
<p>Count: {count}</p>
<Button onClick={increment}>Increment</Button>
</div>
);
}
Global state is suitable when:
- State needs to be accessed by many components across different parts of the component tree
- State changes need to be synchronized across multiple components
- You need to persist state between page navigations
- State is needed in distant components without "prop drilling"
// Global state with Context API for widely shared state
// In theme-context.tsx
import { createContext, useContext, useState } from 'react';
type ThemeContextType = {
theme: 'light' | 'dark';
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// In theme-toggle.tsx (consumer component)
import { useTheme } from '@/context/theme-context';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<Button onClick={toggleTheme} variant="ghost">
{theme === 'light' ? 'Dark' : 'Light'} Mode
</Button>
);
}
// Application structure - global providers
// In layout.tsx
import { ThemeProvider } from '@/context/theme-context';
import { AuthProvider } from '@/context/auth-context';
export function RootLayout({ children }) {
return (
<AuthProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</AuthProvider>
);
}
State Management Approaches
Choose the appropriate state management pattern based on complexity:
1. Component + Custom Hook (Default Approach)
For component-specific state that doesn't need sharing:
// In use-form.ts
function useForm<T>(initialValues: T) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
// Form validation and submission logic
return { values, errors, handleChange /* ...other methods */ };
}
// In register-form.tsx
function RegisterForm() {
const { values, errors, handleChange, handleSubmit } = useForm({
email: '',
password: '',
name: ''
});
// Render form using values and handlers
}
2. Context + Hook Pattern (For Shared State)
For state that needs to be accessed across multiple components:
// In todo-context.tsx
import { createContext, useContext, useState, useCallback } from 'react';
type Todo = {
id: string;
text: string;
completed: boolean;
};
type TodoContextType = {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
};
const TodoContext = createContext<TodoContextType | undefined>(undefined);
// File follows kebab-case: todo-provider.tsx
export function TodoProvider({ children }) {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = useCallback((text: string) => {
setTodos(prev => [
...prev,
{ id: crypto.randomUUID(), text, completed: false }
]);
}, []);
const toggleTodo = useCallback((id: string) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const deleteTodo = useCallback((id: string) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<TodoContext.Provider value={{ todos, addTodo, toggleTodo, deleteTodo }}>
{children}
</TodoContext.Provider>
);
}
// Custom hook for consuming the context
export function useTodos() {
const context = useContext(TodoContext);
if (context === undefined) {
throw new Error('useTodos must be used within a TodoProvider');
}
return context;
}
// In todo-list.tsx
import { useTodos } from '@/context/todo-context';
export function TodoList() {
const { todos, toggleTodo, deleteTodo } = useTodos();
return (
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
3. Context + Reducer Pattern (For Complex State)
For complex state with many related operations:
// In todo-context.tsx
// Define action types with union type
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: string }
| { type: 'DELETE_TODO'; id: string }
| { type: 'SET_FILTER'; filter: FilterType };
type Todo = {
id: string;
text: string;
completed: boolean;
};
type FilterType = 'all' | 'active' | 'completed';
type TodoState = {
todos: Todo[];
filter: FilterType;
};
// Create reducer in separate file: todo-reducer.ts
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: crypto.randomUUID(),
text: action.text,
completed: false
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id)
};
case 'SET_FILTER':
return {
...state,
filter: action.filter
};
default:
return state;
}
}
// Create context provider with action creators
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all'
});
// Action creators for better ergonomics
const addTodo = useCallback((text: string) => {
dispatch({ type: 'ADD_TODO', text });
}, []);
const toggleTodo = useCallback((id: string) => {
dispatch({ type: 'TOGGLE_TODO', id });
}, []);
const deleteTodo = useCallback((id: string) => {
dispatch({ type: 'DELETE_TODO', id });
}, []);
const setFilter = useCallback((filter: FilterType) => {
dispatch({ type: 'SET_FILTER', filter });
}, []);
// Derived state (computed values)
const filteredTodos = useMemo(() => {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed);
case 'completed':
return state.todos.filter(todo => todo.completed);
default:
return state.todos;
}
}, [state.todos, state.filter]);
const value = {
todos: filteredTodos,
filter: state.filter,
addTodo,
toggleTodo,
deleteTodo,
setFilter
};
return (
<TodoContext.Provider value={value}>
{children}
</TodoContext.Provider>
);
}
Testing Strategy
Classicist over Mockist
Test behavior, not implementation:
// Bad: Testing implementation details
test('toggleDarkMode should set isDarkMode to true', () => {
const wrapper = mount(<ThemeProvider />);
expect(wrapper.state('isDarkMode')).toBe(false);
wrapper.instance().toggleDarkMode();
expect(wrapper.state('isDarkMode')).toBe(true);
});
// Good: Testing observable behavior
test('clicking theme toggle should switch between light and dark mode', () => {
render(<App />);
// Initial state (light theme)
expect(screen.getByText('Switch to dark mode')).toBeInTheDocument();
expect(document.body).toHaveClass('light-theme');
// Click the toggle
userEvent.click(screen.getByText('Switch to dark mode'));
// Check that UI changed correctly
expect(screen.getByText('Switch to light mode')).toBeInTheDocument();
expect(document.body).toHaveClass('dark-theme');
});
Integration Testing Over Unit Testing
Focus on how components work together:
// Integration test example
test('completing a todo should move it to the completed list', async () => {
// Render component with real context providers
render(
<TodoProvider>
<TodoApp />
</TodoProvider>
);
// Add a new todo
const input = screen.getByPlaceholderText('Add a todo');
userEvent.type(input, 'Buy milk');
userEvent.click(screen.getByText('Add'));
// Verify it's in the active list
expect(screen.getByTestId('active-list')).toHaveTextContent('Buy milk');
// Complete the todo
userEvent.click(screen.getByLabelText('Mark as completed'));
// Verify it moved to completed list
expect(screen.getByTestId('completed-list')).toHaveTextContent('Buy milk');
expect(screen.getByTestId('active-list')).not.toHaveTextContent('Buy milk');
});
Performance Optimization
React-Specific Optimizations
// Memoize expensive calculations
function ProductList({ products, filter }) {
// Memoize filtered products calculation
const filteredProducts = useMemo(() => {
console.log('Filtering products'); // Should only log when dependencies change
return products.filter(product =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
// Memoize components that render often
const ProductItem = memo(function ProductItem({ product, onAddToCart }) {
console.log(`Rendering product: ${product.id}`);
return (
<div className="product">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => onAddToCart(product)}>
Add to Cart
</button>
</div>
);
});
// Memoize callback functions to prevent unnecessary re-renders
function ShoppingCart() {
const [items, setItems] = useState([]);
// Without useCallback, this function would be recreated on every render
const addToCart = useCallback((product) => {
setItems(prevItems => [...prevItems, product]);
}, []);
return (
<div>
<h2>Cart ({items.length})</h2>
<ProductList
products={availableProducts}
onAddToCart={addToCart}
/>
</div>
);
}
Accessibility
Key Principles
// Bad: Inaccessible button
<div
className="btn"
onClick={handleClick}
>
<img src="/icons/delete.svg" />
</div>
// Good: Accessible button with proper semantics
<button
className="btn"
onClick={handleClick}
aria-label="Delete item"
>
<img src="/icons/delete.svg" alt="" role="presentation" />
</button>
// Form with proper accessibility
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name-input">Name</label>
<input
id="name-input"
type="text"
value={name}
onChange={e => setName(e.target.value)}
aria-required="true"
aria-invalid={!!errors.name}
/>
{errors.name && (
<div className="error" role="alert">
{errors.name}
</div>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
aria-busy={isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
Code Review Process
Review Checklist
Review Checklist
- Functionality: Does the code work as intended? Does it achieve all acceptance criteria indicated on the respective ticket?
- Architecture: Does the code follow our architectural patterns?
- Code Style: Does the code follow our code style guidelines?
- Performance: Are there any obvious performance issues?
- Security: Are there potential security vulnerabilities?
- Accessibility: Does the code meet accessibility standards?
- Maintainability: Will this code be easy to maintain?
- Testing: Are tests adequate and do they pass?
- Documentation: Is the code well-documented where needed?