TypeScript React Project - Beginner CLAUDE.md Example

Project Context

This is a simple React TypeScript application for a todo list. The project uses:

  • React 18 with TypeScript
  • Vite as the build tool
  • CSS Modules for styling
  • Jest for testing

Code Style Guidelines

TypeScript Configuration

// Always use strict mode
"strict": true
"noImplicitAny": true
"strictNullChecks": true

Component Patterns

  • Use functional components with hooks
  • Prefer named exports for components
  • Use proper TypeScript types for props

Good Practice Example:

// ✅ Good: Properly typed functional component
interface TodoItemProps {
  id: string;
  text: string;
  completed: boolean;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}
 
export const TodoItem: React.FC<TodoItemProps> = ({ 
  id, 
  text, 
  completed, 
  onToggle, 
  onDelete 
}) => {
  return (
    <div className={styles.todoItem}>
      <input 
        type="checkbox" 
        checked={completed} 
        onChange={() => onToggle(id)} 
      />
      <span className={completed ? styles.completed : ''}>{text}</span>
      <button onClick={() => onDelete(id)}>Delete</button>
    </div>
  );
};

Bad Practice Example:

// ❌ Bad: No types, using any
export const TodoItem = (props: any) => {
  return (
    <div>
      <input type="checkbox" checked={props.completed} />
      <span>{props.text}</span>
    </div>
  );
};

Common Patterns

State Management

Use useState with proper typing:

// Define the shape of your state
interface Todo {
  id: string;
  text: string;
  completed: boolean;
  createdAt: Date;
}
 
// Use the type in useState
const [todos, setTodos] = useState<Todo[]>([]);

Event Handlers

Always type your event handlers:

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // Handle form submission
};
 
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

Custom Hooks

Create reusable logic with custom hooks:

// Custom hook for local storage
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error loading ${key} from localStorage:`, error);
      return initialValue;
    }
  });
 
  const setValue = (value: T) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`Error saving ${key} to localStorage:`, error);
    }
  };
 
  return [storedValue, setValue];
}

Error Handling

Always handle errors gracefully:

// ✅ Good: Proper error handling
const fetchTodos = async (): Promise<Todo[]> => {
  try {
    const response = await fetch('/api/todos');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch todos:', error);
    // Show user-friendly error message
    return [];
  }
};
 
// ❌ Bad: No error handling
const fetchTodos = async () => {
  const response = await fetch('/api/todos');
  return response.json();
};

Testing Guidelines

Write tests for all components:

import { render, screen, fireEvent } from '@testing-library/react';
import { TodoItem } from './TodoItem';
 
describe('TodoItem', () => {
  const mockProps = {
    id: '1',
    text: 'Test todo',
    completed: false,
    onToggle: jest.fn(),
    onDelete: jest.fn(),
  };
 
  it('renders todo text', () => {
    render(<TodoItem {...mockProps} />);
    expect(screen.getByText('Test todo')).toBeInTheDocument();
  });
 
  it('calls onToggle when checkbox is clicked', () => {
    render(<TodoItem {...mockProps} />);
    const checkbox = screen.getByRole('checkbox');
    fireEvent.click(checkbox);
    expect(mockProps.onToggle).toHaveBeenCalledWith('1');
  });
});

Performance Considerations

Memoization

Use React.memo for expensive components:

export const ExpensiveTodoList = React.memo<TodoListProps>(({ todos, onUpdate }) => {
  // Component that renders many items
  return (
    <div>
      {todos.map(todo => (
        <TodoItem key={todo.id} {...todo} onUpdate={onUpdate} />
      ))}
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison function
  return prevProps.todos.length === nextProps.todos.length &&
         prevProps.todos.every((todo, index) => 
           todo.id === nextProps.todos[index].id &&
           todo.completed === nextProps.todos[index].completed
         );
});

useCallback and useMemo

Optimize callbacks and computed values:

const TodoApp: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
 
  // Memoize filtered todos
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
 
  // Memoize callback to prevent unnecessary re-renders
  const handleToggle = useCallback((id: string) => {
    setTodos(prev => prev.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);
 
  return (
    <div>
      {/* Component JSX */}
    </div>
  );
};

Common Mistakes to Avoid

  1. Not handling loading states
// ❌ Bad
const App = () => {
  const [data, setData] = useState(null);
  
  return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>;
};
 
// ✅ Good
const App = () => {
  const [data, setData] = useState<Item[] | null>(null);
  const [loading, setLoading] = useState(true);
  
  if (loading) return <LoadingSpinner />;
  if (!data) return <EmptyState />;
  
  return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>;
};
  1. Using array index as key in dynamic lists
// ❌ Bad: Can cause issues with reordering
{todos.map((todo, index) => <TodoItem key={index} {...todo} />)}
 
// ✅ Good: Use stable unique ID
{todos.map(todo => <TodoItem key={todo.id} {...todo} />)}
  1. Mutating state directly
// ❌ Bad: Mutating state
const handleComplete = (id: string) => {
  const todo = todos.find(t => t.id === id);
  todo.completed = true; // Direct mutation!
  setTodos(todos); // React won't re-render
};
 
// ✅ Good: Create new state
const handleComplete = (id: string) => {
  setTodos(todos.map(todo => 
    todo.id === id ? { ...todo, completed: true } : todo
  ));
};

Debugging Tips

  1. Use React DevTools Profiler to identify performance issues
  2. Enable “Highlight updates” to see unnecessary re-renders
  3. Use console.log with descriptive labels:
console.log('[TodoApp] Rendering with todos:', todos.length);
  1. Create a debug component for development:
const DebugTodos: React.FC<{ todos: Todo[] }> = ({ todos }) => {
  if (process.env.NODE_ENV !== 'development') return null;
  
  return (
    <pre style={{ background: '#f0f0f0', padding: '10px' }}>
      {JSON.stringify(todos, null, 2)}
    </pre>
  );
};

File Structure Convention

src/
├── components/
│   ├── TodoItem/
│   │   ├── TodoItem.tsx
│   │   ├── TodoItem.test.tsx
│   │   ├── TodoItem.module.css
│   │   └── index.ts
│   └── TodoList/
│       ├── TodoList.tsx
│       ├── TodoList.test.tsx
│       └── index.ts
├── hooks/
│   ├── useLocalStorage.ts
│   └── useTodos.ts
├── types/
│   └── todo.ts
├── utils/
│   ├── todoHelpers.ts
│   └── todoHelpers.test.ts
├── App.tsx
├── App.test.tsx
└── main.tsx

Workshop Exercise Instructions

When working on exercises:

  1. Always start by understanding the existing code structure
  2. Write types first, then implement functionality
  3. Test your changes manually before writing unit tests
  4. Commit often with descriptive messages
  5. Ask questions if TypeScript errors are unclear

Remember: TypeScript is here to help you catch errors early. Don’t fight it, embrace it!