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": trueComponent 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
- 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>;
};- 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} />)}- 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
- Use React DevTools Profiler to identify performance issues
- Enable “Highlight updates” to see unnecessary re-renders
- Use console.log with descriptive labels:
console.log('[TodoApp] Rendering with todos:', todos.length);- 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:
- Always start by understanding the existing code structure
- Write types first, then implement functionality
- Test your changes manually before writing unit tests
- Commit often with descriptive messages
- Ask questions if TypeScript errors are unclear
Remember: TypeScript is here to help you catch errors early. Don’t fight it, embrace it!