Skip to main content

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:

Key Benefits
  • 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.

Anti-pattern
// 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>;
}
Best Practice
// 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.

Anti-pattern
// 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
}
Best Practice
// 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.

Anti-pattern
// Bad: Direct dependency on implementation
function UserService() {
this.repository = new MySQLUserRepository();

this.getUser = (id) => {
return this.repository.findById(id);
};
}
Best Practice
// 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.

Anti-pattern
// 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;
}
Best Practice
// 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.

Anti-pattern
// 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 };
}
Best Practice
// 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.

Anti-pattern
// 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"
};
}
Best Practice
// 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.

Anti-pattern
// Bad: Silent failure
function transferMoney(fromAccount, toAccount, amount) {
if (fromAccount.balance >= amount) {
fromAccount.balance -= amount;
toAccount.balance += amount;
return true;
}
return false; // Silently fails
}
Best Practice
// 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.

Anti-pattern
// Bad: Deep inheritance hierarchy
class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class ElectricCar extends Car { /* ... */ }
class ElectricSUV extends ElectricCar { /* ... */ }
Best Practice
// 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.

Anti-pattern
// 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 };
}
Best Practice
// 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.

Anti-pattern
// 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);
}
}
Best Practice
// 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:

TypeConventionExample
File namingUse kebab-case for all filesuser-profile.tsx, use-user-profile.ts
Component namingUse PascalCase for component namesUserProfile
Hook namingUse camelCase with use prefixuseUserProfile
Directory structureGroup related components in feature directoriesSee 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:

Anti-pattern
// 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>
);
}
Best Practice
// 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

ShadCN Advantages
  • 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

Decision Tree

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:

  1. State needs to be accessed by many components across different parts of the component tree
  2. State changes need to be synchronized across multiple components
  3. You need to persist state between page navigations
  4. 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:

Anti-pattern
// 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);
});
Best Practice
// 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

Anti-pattern
// Bad: Inaccessible button
<div
className="btn"
onClick={handleClick}
>
<img src="/icons/delete.svg" />
</div>
Best Practice
// 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

  1. Functionality: Does the code work as intended? Does it achieve all acceptance criteria indicated on the respective ticket?
  2. Architecture: Does the code follow our architectural patterns?
  3. Code Style: Does the code follow our code style guidelines?
  4. Performance: Are there any obvious performance issues?
  5. Security: Are there potential security vulnerabilities?
  6. Accessibility: Does the code meet accessibility standards?
  7. Maintainability: Will this code be easy to maintain?
  8. Testing: Are tests adequate and do they pass?
  9. Documentation: Is the code well-documented where needed?