DEV Community

Shashank Trivedi
Shashank Trivedi

Posted on

Hands-On Workshop: Refactoring Anti-Patterns

Exercise 1: Spot the Anti-Patterns (20 minutes)

Instructions

Review each code snippet and identify the anti-patterns. Write down:

  1. What anti-pattern(s) you see
  2. Why it's problematic
  3. 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Anti-Patterns Present:

  1. ***
  2. ***
  3. ***
  4. ***

Click to see answers

Anti-Patterns Identified:

  1. God Component - Component does too much (fetching, state management, rendering)
  2. Poor Error Handling - Errors not handled consistently, no retry mechanism
  3. Inline Styles - All styles inline, not reusable
  4. Duplicated Logic - Three nearly identical fetch calls
  5. State Management - Too many separate loading/error states
  6. 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:

  1. Custom hooks for data fetching
  2. Smaller, focused components
  3. Proper TypeScript types
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Your Implementation:

// Write your solution here
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

Your Implementation:

// Write your solutions here
Enter fullscreen mode Exit fullscreen mode

Step 4: Create CSS Module

/* UserDashboard.module.css */
/* YOUR CODE HERE */
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

Your Implementation:

// Write your solution here
Enter fullscreen mode Exit fullscreen mode

Click to see solution

// hooks/useUserData.ts
export function useUserData(userId: string): UseUserDataResult {
  const [state, setState] = useState&lt;{
    user: User | null;
    posts: Post[];
    comments: Comment[];
    loading: boolean;
    error: Error | null;
  }&gt;({
    user: null,
    posts: [],
    comments: [],
    loading: false,
    error: null,
  });

  const fetchData = useCallback(async () =&gt; {
    setState((prev) =&gt; ({ ...prev, loading: true, error: null }));

    try {
      const [userData, postsData, commentsData] = await Promise.all([
        fetch(`/api/user/${userId}`).then((r) =&gt; r.json()),
        fetch(`/api/posts/${userId}`).then((r) =&gt; r.json()),
        fetch(`/api/comments/${userId}`).then((r) =&gt; r.json()),
      ]);

      setState({
        user: userData,
        posts: postsData,
        comments: commentsData,
        loading: false,
        error: null,
      });
    } catch (error) {
      setState((prev) =&gt; ({
        ...prev,
        loading: false,
        error: error instanceof Error ? error : new Error('Failed to fetch data'),
      }));
    }
  }, [userId]);

  useEffect(() =&gt; {
    fetchData();
  }, [fetchData]);

  return { ...state, refetch: fetchData };
}

// components/UserDashboard/index.tsx
export const UserDashboard = ({ userId }: { userId: string }) =&gt; {
  const { user, posts, comments, loading, error, refetch } = useUserData(userId);

  if (loading) return ;
  if (error) return ;
  if (!user) return ;

  return (







  );
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Your Task

  1. Extract all magic numbers and strings to named constants
  2. Make error messages clear and informative
  3. Return detailed validation results instead of just boolean

Your Refactored Code:

// Write your solution here
Enter fullscreen mode Exit fullscreen mode

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: /[!@#$%^&amp;*]/,
  },
} 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 &lt; PASSWORD_CONSTRAINTS.MIN_LENGTH) {
    errors.push(`Password must be at least ${PASSWORD_CONSTRAINTS.MIN_LENGTH} characters`);
  }

  if (password.length &gt; 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 (!@#$%^&amp;*)');
  }

  return {
    valid: errors.length === 0,
    errors,
  };
}

export function validateUsername(username: string): ValidationResult {
  const errors: string[] = [];

  if (username.length &lt; USERNAME_CONSTRAINTS.MIN_LENGTH) {
    errors.push(`Username must be at least ${USERNAME_CONSTRAINTS.MIN_LENGTH} characters`);
  }

  if (username.length &gt; 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 &gt; 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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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';
  }
}
Enter fullscreen mode Exit fullscreen mode

Your Task

Refactor to use configuration objects instead of switch statements.

Your Refactored Code:

// Write your solution here
Enter fullscreen mode Exit fullscreen mode

Click to see solution

// constants/notifications.ts
import { InfoIcon, CheckIcon, AlertIcon, ErrorIcon } from '@/components/icons';

interface NotificationConfig {
  icon: React.ComponentType&lt;{ size: number }&gt;;
  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 }) =&gt; {
  const config = getNotificationConfig(notification.type);
  const IconComponent = config.icon;

  return (


      <h3>{config.title}</h3>
      <p>{notification.message}</p>

  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Your Task

  1. Create Context for theme, user, and language
  2. Create custom hooks to access these values
  3. 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
Enter fullscreen mode Exit fullscreen mode

Click to see solution

// contexts/AppContext.tsx
interface AppContextValue {
  theme: Theme;
  user: User | null;
  language: string;
  setTheme: (theme: Theme) =&gt; void;
  setUser: (user: User | null) =&gt; void;
  setLanguage: (language: string) =&gt; void;
}

const AppContext = createContext(undefined);

export const AppProvider = ({ children }: { children: React.ReactNode }) =&gt; {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);
  const [language, setLanguage] = useState('en');

  const value = useMemo(
    () =&gt; ({
      theme,
      user,
      language,
      setTheme,
      setUser,
      setLanguage,
    }),
    [theme, user, language],
  );

  return {children};
};

// hooks/useApp.ts
export const useApp = () =&gt; {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within AppProvider');
  }
  return context;
};

export const useTheme = () =&gt; {
  const { theme, setTheme } = useApp();
  return { theme, setTheme };
};

export const useUser = () =&gt; {
  const { user, setUser } = useApp();
  return { user, setUser };
};

export const useLanguage = () =&gt; {
  const { language, setLanguage } = useApp();
  return { language, setLanguage };
};

// Refactored App.tsx
const App = () =&gt; {
  return (














  );
};

// Components consume only what they need
const UserMenu = () =&gt; {
  const { user } = useUser();
  const { theme, setTheme } = useTheme();
  // Use user and theme
};

const Header = () =&gt; {
  const { user } = useUser();
  // Only needs user
};
Enter fullscreen mode Exit fullscreen mode

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])} />;
};
Enter fullscreen mode Exit fullscreen mode

Your Task

Add proper loading and error states, user feedback, and retry mechanisms.

Your Solution:

// Write your solution here
Enter fullscreen mode Exit fullscreen mode

Click to see solution

// hooks/useDataFetch.ts
interface UseDataFetchState {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

export function useDataFetch(fetchFn: () =&gt; Promise) {
  const [state, setState] = useState&gt;({
    data: null,
    loading: false,
    error: null,
  });

  const execute = useCallback(async () =&gt; {
    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 = () =&gt; {
  const { data, loading, error, refetch } = useDataFetch(() =&gt;
    fetch('/api/data').then((res) =&gt; {
      if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
      return res.json();
    }),
  );

  useEffect(() =&gt; {
    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) =&gt; (
        {item.name}
      ))}

  );
};

// Refactored FileUploader
const FileUploader = () =&gt; {
  const [uploadState, setUploadState] = useState&lt;{
    status: 'idle' | 'uploading' | 'success' | 'error';
    message?: string;
    progress?: number;
  }&gt;({ status: 'idle' });

  const handleUpload = async (file: File) =&gt; {
    setUploadState({ status: 'uploading', progress: 0 });

    try {
      await uploadFileWithProgress(file, (progress) =&gt; {
        setUploadState({ status: 'uploading', progress });
      });

      setUploadState({
        status: 'success',
        message: `Successfully uploaded ${file.name}`,
      });

      // Auto-clear after 3 seconds
      setTimeout(() =&gt; {
        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] &amp;&amp; handleUpload(e.target.files[0])}
        disabled={uploadState.status === 'uploading'}
      /&gt;

      {uploadState.status === 'uploading' &amp;&amp; }

      {uploadState.message &amp;&amp; (
         setUploadState({ status: 'idle' })}
        /&gt;
      )}

  );
};
Enter fullscreen mode Exit fullscreen mode

Bonus Exercise: Real Codebase Refactoring (30+ minutes)

Task

  1. Find a file in your codebase with anti-patterns
  2. Identify 2-3 specific anti-patterns
  3. Refactor the code
  4. Create a PR with before/after comparison
  5. 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:

  1. Which anti-pattern is most common in our codebase?
  2. Which refactoring had the biggest impact?
  3. What obstacles do we face when refactoring?
  4. How can we prevent anti-patterns in new code?
  5. 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:

  1. What worked well in this workshop?
  2. What could be improved?
  3. Which exercises were most valuable?
  4. What topics would you like to cover next?
  5. 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)