Exercise 1: Spot the Anti-Patterns (20 minutes)
Instructions
Review each code snippet and identify the anti-patterns. Write down:
- What anti-pattern(s) you see
- Why it's problematic
- What principle(s) it violates
Code Snippet A: User Dashboard
const UserDashboard = () => {
const [user, setUser] = useState();
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [likes, setLikes] = useState(0);
const [loading1, setLoading1] = useState(false);
const [loading2, setLoading2] = useState(false);
const [loading3, setLoading3] = useState(false);
const [error1, setError1] = useState();
const [error2, setError2] = useState();
const [error3, setError3] = useState();
useEffect(() => {
setLoading1(true);
fetch('/api/user/123')
.then((res) => res.json())
.then((data) => {
setUser(data);
setLoading1(false);
})
.catch((e) => {
setError1(e);
setLoading1(false);
});
setLoading2(true);
fetch('/api/posts/123')
.then((res) => res.json())
.then((data) => {
setPosts(data);
setLoading2(false);
});
setLoading3(true);
fetch('/api/comments/123')
.then((res) => res.json())
.then((data) => {
setComments(data);
setLoading3(false);
});
}, []);
if (loading1 || loading2 || loading3) return <div>Loading...</div>;
if (error1) return <div>Error: {error1}</div>;
return (
<div style={{ padding: '20px', backgroundColor: '#f5f5f5' }}>
<div style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>
{user ? user.name : 'Unknown User'}
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<div style={{ flex: 1, padding: '16px', backgroundColor: 'white', borderRadius: '8px' }}>
<h2 style={{ fontSize: '18px', marginBottom: '8px' }}>Posts ({posts.length})</h2>
{posts.map((post) => (
<div key={post.id} style={{ marginBottom: '8px', padding: '8px', border: '1px solid #ddd' }}>
{post.title}
</div>
))}
</div>
<div style={{ flex: 1, padding: '16px', backgroundColor: 'white', borderRadius: '8px' }}>
<h2 style={{ fontSize: '18px', marginBottom: '8px' }}>Comments ({comments.length})</h2>
{comments.map((comment) => (
<div key={comment.id} style={{ marginBottom: '8px', padding: '8px', border: '1px solid #ddd' }}>
{comment.text}
</div>
))}
</div>
</div>
</div>
);
};
Anti-Patterns Present:
- ***
- ***
- ***
- ***
Click to see answers
Anti-Patterns Identified:
- God Component - Component does too much (fetching, state management, rendering)
- Poor Error Handling - Errors not handled consistently, no retry mechanism
- Inline Styles - All styles inline, not reusable
- Duplicated Logic - Three nearly identical fetch calls
- State Management - Too many separate loading/error states
- Magic Numbers - Hardcoded user ID '123'
Principles Violated:
- Single Responsibility
- DRY (Don't Repeat Yourself)
- Separation of Concerns
Exercise 2: Refactor the God Component (30 minutes)
Your Task
Refactor the UserDashboard from Exercise 1. Split it into:
- Custom hooks for data fetching
- Smaller, focused components
- Proper TypeScript types
- CSS modules for styling
Step-by-Step Guide
Step 1: Define Types
// types/user.ts
export interface User {
id: string;
name: string;
email: string;
}
export interface Post {
id: string;
title: "string;"
content: string;
userId: string;
}
export interface Comment {
id: string;
text: string;
postId: string;
userId: string;
}
Step 2: Create Data Fetching Hook
// hooks/useUserData.ts
interface UseUserDataResult {
user: User | null;
posts: Post[];
comments: Comment[];
loading: boolean;
error: Error | null;
refetch: () => void;
}
export function useUserData(userId: string): UseUserDataResult {
// YOUR CODE HERE
// Hint: Use a single state object for all data
// Hint: Fetch all data in one useEffect
// Hint: Include proper error handling
}
Your Implementation:
// Write your solution here
Step 3: Create Sub-Components
// components/UserDashboard/UserHeader.tsx
interface UserHeaderProps {
user: User;
}
export const UserHeader = ({ user }: UserHeaderProps) => {
// YOUR CODE HERE
};
// components/UserDashboard/PostsList.tsx
interface PostsListProps {
posts: Post[];
}
export const PostsList = ({ posts }: PostsListProps) => {
// YOUR CODE HERE
};
// components/UserDashboard/CommentsList.tsx
interface CommentsListProps {
comments: Comment[];
}
export const CommentsList = ({ comments }: CommentsListProps) => {
// YOUR CODE HERE
};
Your Implementation:
// Write your solutions here
Step 4: Create CSS Module
/* UserDashboard.module.css */
/* YOUR CODE HERE */
Step 5: Refactor Main Component
// components/UserDashboard/index.tsx
import styles from './UserDashboard.module.css';
import { useUserData } from '../../hooks/useUserData';
import { UserHeader } from './UserHeader';
import { PostsList } from './PostsList';
import { CommentsList } from './CommentsList';
export const UserDashboard = ({ userId }: { userId: string }) => {
// YOUR CODE HERE
};
Your Implementation:
// Write your solution here
Click to see solution
// hooks/useUserData.ts
export function useUserData(userId: string): UseUserDataResult {
const [state, setState] = useState<{
user: User | null;
posts: Post[];
comments: Comment[];
loading: boolean;
error: Error | null;
}>({
user: null,
posts: [],
comments: [],
loading: false,
error: null,
});
const fetchData = useCallback(async () => {
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const [userData, postsData, commentsData] = await Promise.all([
fetch(`/api/user/${userId}`).then((r) => r.json()),
fetch(`/api/posts/${userId}`).then((r) => r.json()),
fetch(`/api/comments/${userId}`).then((r) => r.json()),
]);
setState({
user: userData,
posts: postsData,
comments: commentsData,
loading: false,
error: null,
});
} catch (error) {
setState((prev) => ({
...prev,
loading: false,
error: error instanceof Error ? error : new Error('Failed to fetch data'),
}));
}
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { ...state, refetch: fetchData };
}
// components/UserDashboard/index.tsx
export const UserDashboard = ({ userId }: { userId: string }) => {
const { user, posts, comments, loading, error, refetch } = useUserData(userId);
if (loading) return ;
if (error) return ;
if (!user) return ;
return (
);
};
Exercise 3: Eliminate Magic Numbers (15 minutes)
Code to Refactor
// File: utils/validation.ts
export function validatePassword(password: string): boolean {
if (password.length < 8) return false;
if (password.length > 128) return false;
if (!/[A-Z]/.test(password)) return false;
if (!/[a-z]/.test(password)) return false;
if (!/[0-9]/.test(password)) return false;
if (!/[!@#$%^&*]/.test(password)) return false;
return true;
}
export function validateUsername(username: string): boolean {
if (username.length < 3) return false;
if (username.length > 20) return false;
if (!/^[a-zA-Z0-9_]+$/.test(username)) return false;
return true;
}
export function validateFile(file: File): boolean {
const maxSize = 10485760; // bytes
if (file.size > maxSize) return false;
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) return false;
return true;
}
Your Task
- Extract all magic numbers and strings to named constants
- Make error messages clear and informative
- Return detailed validation results instead of just boolean
Your Refactored Code:
// Write your solution here
Click to see solution
// constants/validation.ts
export const PASSWORD_CONSTRAINTS = {
MIN_LENGTH: 8,
MAX_LENGTH: 128,
REQUIRED_PATTERNS: {
UPPERCASE: /[A-Z]/,
LOWERCASE: /[a-z]/,
DIGIT: /[0-9]/,
SPECIAL: /[!@#$%^&*]/,
},
} as const;
export const USERNAME_CONSTRAINTS = {
MIN_LENGTH: 3,
MAX_LENGTH: 20,
ALLOWED_PATTERN: /^[a-zA-Z0-9_]+$/,
} as const;
export const FILE_UPLOAD_CONSTRAINTS = {
MAX_SIZE_BYTES: 10 * 1024 * 1024, // 10MB
ALLOWED_MIME_TYPES: ['image/jpeg', 'image/png', 'image/gif'] as const,
} as const;
// types/validation.ts
export interface ValidationResult {
valid: boolean;
errors: string[];
}
// utils/validation.ts
export function validatePassword(password: string): ValidationResult {
const errors: string[] = [];
if (password.length < PASSWORD_CONSTRAINTS.MIN_LENGTH) {
errors.push(`Password must be at least ${PASSWORD_CONSTRAINTS.MIN_LENGTH} characters`);
}
if (password.length > PASSWORD_CONSTRAINTS.MAX_LENGTH) {
errors.push(`Password must be no more than ${PASSWORD_CONSTRAINTS.MAX_LENGTH} characters`);
}
if (!PASSWORD_CONSTRAINTS.REQUIRED_PATTERNS.UPPERCASE.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!PASSWORD_CONSTRAINTS.REQUIRED_PATTERNS.LOWERCASE.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!PASSWORD_CONSTRAINTS.REQUIRED_PATTERNS.DIGIT.test(password)) {
errors.push('Password must contain at least one digit');
}
if (!PASSWORD_CONSTRAINTS.REQUIRED_PATTERNS.SPECIAL.test(password)) {
errors.push('Password must contain at least one special character (!@#$%^&*)');
}
return {
valid: errors.length === 0,
errors,
};
}
export function validateUsername(username: string): ValidationResult {
const errors: string[] = [];
if (username.length < USERNAME_CONSTRAINTS.MIN_LENGTH) {
errors.push(`Username must be at least ${USERNAME_CONSTRAINTS.MIN_LENGTH} characters`);
}
if (username.length > USERNAME_CONSTRAINTS.MAX_LENGTH) {
errors.push(`Username must be no more than ${USERNAME_CONSTRAINTS.MAX_LENGTH} characters`);
}
if (!USERNAME_CONSTRAINTS.ALLOWED_PATTERN.test(username)) {
errors.push('Username can only contain letters, numbers, and underscores');
}
return {
valid: errors.length === 0,
errors,
};
}
export function validateFile(file: File): ValidationResult {
const errors: string[] = [];
if (file.size > FILE_UPLOAD_CONSTRAINTS.MAX_SIZE_BYTES) {
const maxSizeMB = FILE_UPLOAD_CONSTRAINTS.MAX_SIZE_BYTES / 1024 / 1024;
errors.push(`File size must be less than ${maxSizeMB}MB`);
}
if (!FILE_UPLOAD_CONSTRAINTS.ALLOWED_MIME_TYPES.includes(file.type as any)) {
errors.push(`File type must be one of: ${FILE_UPLOAD_CONSTRAINTS.ALLOWED_MIME_TYPES.join(', ')}`);
}
return {
valid: errors.length === 0,
errors,
};
}
Exercise 4: Replace Switch Statement (15 minutes)
Code to Refactor
interface Notification {
type: 'info' | 'success' | 'warning' | 'error';
message: string;
}
export function getNotificationIcon(type: Notification['type']): React.ReactNode {
switch (type) {
case 'info':
return <InfoIcon color="blue" size={24} />;
case 'success':
return <CheckIcon color="green" size={24} />;
case 'warning':
return <AlertIcon color="orange" size={24} />;
case 'error':
return <ErrorIcon color="red" size={24} />;
default:
return <InfoIcon color="gray" size={24} />;
}
}
export function getNotificationColor(type: Notification['type']): string {
switch (type) {
case 'info':
return '#3b82f6';
case 'success':
return '#10b981';
case 'warning':
return '#f59e0b';
case 'error':
return '#ef4444';
default:
return '#6b7280';
}
}
export function getNotificationTitle(type: Notification['type']): string {
switch (type) {
case 'info':
return 'Information';
case 'success':
return 'Success';
case 'warning':
return 'Warning';
case 'error':
return 'Error';
default:
return 'Notification';
}
}
Your Task
Refactor to use configuration objects instead of switch statements.
Your Refactored Code:
// Write your solution here
Click to see solution
// constants/notifications.ts
import { InfoIcon, CheckIcon, AlertIcon, ErrorIcon } from '@/components/icons';
interface NotificationConfig {
icon: React.ComponentType<{ size: number }>;
color: string;
title: string;
}
export const NOTIFICATION_CONFIGS: Record = {
info: {
icon: InfoIcon,
color: '#3b82f6',
title: 'Information',
},
success: {
icon: CheckIcon,
color: '#10b981',
title: 'Success',
},
warning: {
icon: AlertIcon,
color: '#f59e0b',
title: 'Warning',
},
error: {
icon: ErrorIcon,
color: '#ef4444',
title: 'Error',
},
} as const;
const DEFAULT_CONFIG: NotificationConfig = {
icon: InfoIcon,
color: '#6b7280',
title: 'Notification',
};
// utils/notifications.ts
export function getNotificationConfig(type: Notification['type']): NotificationConfig {
return NOTIFICATION_CONFIGS[type] ?? DEFAULT_CONFIG;
}
export function getNotificationIcon(type: Notification['type']): React.ReactNode {
const config = getNotificationConfig(type);
const IconComponent = config.icon;
return ;
}
export function getNotificationColor(type: Notification['type']): string {
return getNotificationConfig(type).color;
}
export function getNotificationTitle(type: Notification['type']): string {
return getNotificationConfig(type).title;
}
// Even better: Use the config directly in components
export const NotificationComponent = ({ notification }: { notification: Notification }) => {
const config = getNotificationConfig(notification.type);
const IconComponent = config.icon;
return (
<h3>{config.title}</h3>
<p>{notification.message}</p>
);
};
Exercise 5: Fix Prop Drilling (20 minutes)
Code to Refactor
// App.tsx
const App = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [user, setUser] = useState<User | null>(null);
const [language, setLanguage] = useState('en');
return (
<Layout theme={theme} setTheme={setTheme} user={user} language={language}>
<Dashboard theme={theme} user={user} language={language} setTheme={setTheme}>
<Sidebar theme={theme} user={user} language={language}>
<UserMenu user={user} theme={theme} setTheme={setTheme} language={language} />
<Navigation language={language} theme={theme} />
</Sidebar>
<MainContent theme={theme} user={user} language={language}>
<Header user={user} theme={theme} language={language} />
<Content language={language} />
</MainContent>
</Dashboard>
</Layout>
);
};
Your Task
- Create Context for theme, user, and language
- Create custom hooks to access these values
- Remove prop drilling
Your Solution:
// contexts/AppContext.tsx
// YOUR CODE HERE
// hooks/useTheme.ts, useUser.ts, useLanguage.ts
// YOUR CODE HERE
// Refactored App.tsx
// YOUR CODE HERE
Click to see solution
// contexts/AppContext.tsx
interface AppContextValue {
theme: Theme;
user: User | null;
language: string;
setTheme: (theme: Theme) => void;
setUser: (user: User | null) => void;
setLanguage: (language: string) => void;
}
const AppContext = createContext(undefined);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
const [language, setLanguage] = useState('en');
const value = useMemo(
() => ({
theme,
user,
language,
setTheme,
setUser,
setLanguage,
}),
[theme, user, language],
);
return {children};
};
// hooks/useApp.ts
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
};
export const useTheme = () => {
const { theme, setTheme } = useApp();
return { theme, setTheme };
};
export const useUser = () => {
const { user, setUser } = useApp();
return { user, setUser };
};
export const useLanguage = () => {
const { language, setLanguage } = useApp();
return { language, setLanguage };
};
// Refactored App.tsx
const App = () => {
return (
);
};
// Components consume only what they need
const UserMenu = () => {
const { user } = useUser();
const { theme, setTheme } = useTheme();
// Use user and theme
};
const Header = () => {
const { user } = useUser();
// Only needs user
};
Exercise 6: Improve Error Handling (20 minutes)
Code to Refactor
const DataFetcher = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then(setData)
.catch((err) => console.error(err));
}, []);
return (
<div>
{data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};
const FileUploader = () => {
const handleUpload = async (file: File) => {
try {
await uploadFile(file);
alert('Done!');
} catch (error) {
alert('Failed!');
}
};
return <input type="file" onChange={(e) => handleUpload(e.target.files[0])} />;
};
Your Task
Add proper loading and error states, user feedback, and retry mechanisms.
Your Solution:
// Write your solution here
Click to see solution
// hooks/useDataFetch.ts
interface UseDataFetchState {
data: T | null;
loading: boolean;
error: Error | null;
}
export function useDataFetch(fetchFn: () => Promise) {
const [state, setState] = useState>({
data: null,
loading: false,
error: null,
});
const execute = useCallback(async () => {
setState({ data: null, loading: true, error: null });
try {
const result = await fetchFn();
setState({ data: result, loading: false, error: null });
} catch (error) {
const apiError = error instanceof Error ? error : new Error('An unexpected error occurred');
setState({ data: null, loading: false, error: apiError });
logError(apiError);
}
}, [fetchFn]);
return { ...state, execute, refetch: execute };
}
// Refactored DataFetcher
const DataFetcher = () => {
const { data, loading, error, refetch } = useDataFetch(() =>
fetch('/api/data').then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return res.json();
}),
);
useEffect(() => {
refetch();
}, [refetch]);
if (loading) {
return ;
}
if (error) {
return (
<p>Failed to load data: {error.message}</p>
Retry
);
}
if (!data || data.length === 0) {
return ;
}
return (
{data.map((item) => (
{item.name}
))}
);
};
// Refactored FileUploader
const FileUploader = () => {
const [uploadState, setUploadState] = useState<{
status: 'idle' | 'uploading' | 'success' | 'error';
message?: string;
progress?: number;
}>({ status: 'idle' });
const handleUpload = async (file: File) => {
setUploadState({ status: 'uploading', progress: 0 });
try {
await uploadFileWithProgress(file, (progress) => {
setUploadState({ status: 'uploading', progress });
});
setUploadState({
status: 'success',
message: `Successfully uploaded ${file.name}`,
});
// Auto-clear after 3 seconds
setTimeout(() => {
setUploadState({ status: 'idle' });
}, 3000);
} catch (error) {
const errorMessage = error instanceof Error ? `Upload failed: ${error.message}` : 'An unexpected error occurred';
setUploadState({
status: 'error',
message: errorMessage,
});
logError(error, { context: 'file-upload', fileName: file.name });
}
};
return (
e.target.files?.[0] && handleUpload(e.target.files[0])}
disabled={uploadState.status === 'uploading'}
/>
{uploadState.status === 'uploading' && }
{uploadState.message && (
setUploadState({ status: 'idle' })}
/>
)}
);
};
Bonus Exercise: Real Codebase Refactoring (30+ minutes)
Task
- Find a file in your codebase with anti-patterns
- Identify 2-3 specific anti-patterns
- Refactor the code
- Create a PR with before/after comparison
- Present your refactoring to the group
Checklist
- [ ] Identified specific anti-patterns
- [ ] Wrote proper TypeScript types
- [ ] Extracted magic numbers/strings
- [ ] Improved error handling
- [ ] Added tests
- [ ] Updated documentation
- [ ] Code review feedback addressed
Workshop Wrap-up
Group Discussion (20 minutes)
Questions to discuss:
- Which anti-pattern is most common in our codebase?
- Which refactoring had the biggest impact?
- What obstacles do we face when refactoring?
- How can we prevent anti-patterns in new code?
- What should our team coding standards include?
Action Items
Individual commitments:
- I will refactor *********_********* this week
- I will watch for *********_********* in code reviews
- I will share *********_********* with the team
Team commitments:
- Update code review checklist
- Schedule monthly code quality sessions
- Create ESLint rules for common anti-patterns
- Document team coding standards
Additional Resources
Practice Repos
Reading
- "Refactoring" by Martin Fowler
- "Clean Code" by Robert Martin
- "Effective TypeScript" by Dan Vanderkam
Tools
- ESLint with TypeScript rules
- SonarQube for code quality metrics
- Prettier for consistent formatting
Feedback Form
Please provide feedback:
- What worked well in this workshop?
- What could be improved?
- Which exercises were most valuable?
- What topics would you like to cover next?
- Would you recommend this to other developers?
Remember: Refactoring is a skill that improves with practice.
Don't aim for perfect code - aim for continuous improvement!
Top comments (0)