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
- React Query + Redux: For global server state, React Query owns fetch lifecycle, Redux is read path
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>
);
}
4. React Query + Redux Pattern (For Global Server State)
When server state is consumed by multiple pages or distant components (not just the page that fetches it), use React Query for the fetch lifecycle and Redux as a downstream projection for global access.
Use this when server-fetched data needs to be:
- Read by components outside the fetching page (e.g., a sidebar, header, or unrelated feature)
- Kept fresh after mutations (connect, disconnect, update)
- Available immediately on page transitions without re-fetching
Do NOT manually re-fetch and dispatch to Redux inside mutation onSuccess callbacks. Let React Query's cache invalidation trigger the refetch, and let the query's queryFn handle the Redux sync.
Query Key Factory:
// data-access/query-key-factory.integrations.ts
export const integrationsKeys = {
all: ['integrations'] as const,
instances: (orgId: string) =>
[...integrationsKeys.all, 'instances', orgId] as const,
} as const;
Read Hook (React Query fetches, syncs to Redux):
// data-access/use-integration-instances.ts
export function useIntegrationInstances(orgId: string) {
const service = getEnablementClientService();
const dispatch = useAppDispatch();
return useQuery({
queryKey: integrationsKeys.instances(orgId),
queryFn: async () => {
const [instances, orgProducts] = await Promise.all([
service.getOrgIntegrations(orgId),
service.getOrgProducts(orgId),
]);
// Sync to Redux — global consumers read from selectors
dispatch(setOrgProductData({ orgProducts, integrationInstances: instances }));
return instances;
},
enabled: !!orgId,
});
}
Mutation Hook (invalidates React Query, which refetches and syncs Redux):
// data-access/use-connect-integration.ts
export function useConnectIntegration(orgId: string) {
const queryClient = useQueryClient();
const service = getEnablementClientService();
return useMutation({
mutationFn: (dto: ConnectIntegrationDtoBase) =>
service.connectIntegration(orgId, dto),
onSuccess: () => {
// Invalidate → React Query refetches → queryFn syncs Redux
queryClient.invalidateQueries({
queryKey: integrationsKeys.instances(orgId),
});
toast({ variant: 'success', title: 'Connected', description: '...' });
},
onError: (error) => {
toast({ variant: 'destructive', title: 'Failed', description: getApiClientError(error) });
},
});
}
Page Hook (reads from Redux, React Query provides freshness):
// hooks/use-integrations-page.ts
export function useIntegrationsPage() {
const orgId = useAppSelector(selectActiveOrganization)?._id ?? '';
// Redux is the single source of truth for display
const catalogDefinitions = useAppSelector(selectIntegrationCatalog);
const integrationInstances = useAppSelector(selectIntegrationInstances);
// React Query manages fetch lifecycle — mutations invalidate, refetch syncs Redux
useIntegrationInstances(orgId);
// Mutations invalidate React Query cache on success
const connectMutation = useConnectIntegration(orgId);
const disconnectMutation = useDisconnectIntegration(orgId);
// Build display items from Redux data
const items = useMemo(
() => buildCatalogItems(catalogDefinitions, integrationInstances),
[catalogDefinitions, integrationInstances],
);
// ...
}
Data Flow Summary:
Read: useAppSelector(selectIntegrationInstances) ← Redux (single source of truth)
Refetch: useIntegrationInstances(orgId) ← React Query → queryFn syncs Redux
Mutate: queryClient.invalidateQueries(...) ← triggers refetch → Redux sync
Key principles:
- Redux owns the read path — all components read from Redux selectors
- React Query owns the fetch lifecycle — caching, staleness, background refetch, invalidation
queryFnis the bridge — every successful fetch dispatches to Redux- Mutations never dispatch directly — they invalidate the query key, React Query refetches, and the
queryFnsyncs Redux ProductsInitializerhandles bootstrap — initial app load populates Redux; React Query layers on page-level freshness
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?