On this page
React Component
/**
* React Component Template (TypeScript)
*
* This is a complete, production-ready template for React components.
* Copy this file and modify for your use case.
*
* Key components:
* - TypeScript props interface with JSDoc
* - State management (useState)
* - Data fetching (React Query)
* - Event handlers
* - useEffect with cleanup
* - Loading, error, and empty states
* - Proper TypeScript typing
*/
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// ==============================================================================
// TYPES & INTERFACES
// ==============================================================================
/**
* Example item data structure.
*/
interface Item {
id: number;
name: string;
description: string | null;
price: number;
is_active: boolean;
created_at: string;
}
/**
* Props for the ExampleComponent.
*/
interface ExampleComponentProps {
/** The ID of the user whose items to display */
userId: number;
/** Optional callback when an item is created */
onItemCreate?: (item: Item) => void;
/** Optional callback when an item is updated */
onItemUpdate?: (item: Item) => void;
/** Optional callback when an item is deleted */
onItemDelete?: (itemId: number) => void;
/** Optional CSS class name for custom styling */
className?: string;
/** Whether to show inactive items (default: false) */
showInactive?: boolean;
}
/**
* Form data structure for creating/editing items.
*/
interface ItemFormData {
name: string;
description: string;
price: string; // String for form input, will be parsed to number
is_active: boolean;
}
// ==============================================================================
// API FUNCTIONS
// ==============================================================================
/**
* API client for item operations.
*/
const itemApi = {
/**
* Fetch all items for a user.
*/
fetchItems: async (userId: number, showInactive: boolean = false): Promise<Item[]> => {
const response = await fetch(
`/api/v1/items?is_active=${!showInactive || 'null'}`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
}
);
if (!response.ok) {
throw new Error('Failed to fetch items');
}
return response.json();
},
/**
* Create a new item.
*/
createItem: async (data: Omit<ItemFormData, 'is_active'> & { is_active: boolean }): Promise<Item> => {
const response = await fetch('/api/v1/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({
...data,
price: parseFloat(data.price),
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create item');
}
return response.json();
},
/**
* Update an existing item.
*/
updateItem: async (itemId: number, data: Partial<ItemFormData>): Promise<Item> => {
const response = await fetch(`/api/v1/items/${itemId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({
...data,
price: data.price ? parseFloat(data.price) : undefined,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to update item');
}
return response.json();
},
/**
* Delete an item.
*/
deleteItem: async (itemId: number): Promise<void> => {
const response = await fetch(`/api/v1/items/${itemId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
if (!response.ok) {
throw new Error('Failed to delete item');
}
},
};
// ==============================================================================
// COMPONENT
// ==============================================================================
/**
* Example component demonstrating best practices.
*
* This component manages a list of items with create, update, and delete functionality.
* It demonstrates proper state management, data fetching, error handling, and TypeScript usage.
*
* @component
* @example
* ```tsx
* <ExampleComponent
* userId={123}
* onItemCreate={(item) => console.log('Created:', item)}
* showInactive={false}
* />
* ```
*/
export const ExampleComponent: React.FC<ExampleComponentProps> = ({
userId,
onItemCreate,
onItemUpdate,
onItemDelete,
className = '',
showInactive = false,
}) => {
const queryClient = useQueryClient();
// ==============================================================================
// STATE
// ==============================================================================
const [isCreating, setIsCreating] = useState(false);
const [editingItemId, setEditingItemId] = useState<number | null>(null);
const [formData, setFormData] = useState<ItemFormData>({
name: '',
description: '',
price: '',
is_active: true,
});
// ==============================================================================
// DATA FETCHING (React Query)
// ==============================================================================
/**
* Fetch items query
*/
const {
data: items,
isLoading,
error,
refetch,
} = useQuery({
queryKey: ['items', userId, showInactive],
queryFn: () => itemApi.fetchItems(userId, showInactive),
staleTime: 30000, // Consider data fresh for 30 seconds
refetchOnWindowFocus: false,
});
/**
* Create item mutation
*/
const createMutation = useMutation({
mutationFn: itemApi.createItem,
onSuccess: (newItem) => {
// Invalidate and refetch items query
queryClient.invalidateQueries({ queryKey: ['items'] });
// Call optional callback
onItemCreate?.(newItem);
// Reset form
setIsCreating(false);
setFormData({ name: '', description: '', price: '', is_active: true });
},
onError: (error: Error) => {
console.error('Failed to create item:', error.message);
},
});
/**
* Update item mutation
*/
const updateMutation = useMutation({
mutationFn: ({ itemId, data }: { itemId: number; data: Partial<ItemFormData> }) =>
itemApi.updateItem(itemId, data),
onSuccess: (updatedItem) => {
queryClient.invalidateQueries({ queryKey: ['items'] });
onItemUpdate?.(updatedItem);
setEditingItemId(null);
setFormData({ name: '', description: '', price: '', is_active: true });
},
});
/**
* Delete item mutation
*/
const deleteMutation = useMutation({
mutationFn: itemApi.deleteItem,
onSuccess: (_, itemId) => {
queryClient.invalidateQueries({ queryKey: ['items'] });
onItemDelete?.(itemId);
},
});
// ==============================================================================
// EFFECTS
// ==============================================================================
/**
* Example effect with cleanup
*/
useEffect(() => {
// Setup: Log when component mounts
console.log('ExampleComponent mounted for user:', userId);
// Cleanup: Log when component unmounts
return () => {
console.log('ExampleComponent unmounted');
};
}, [userId]);
/**
* Refetch items when userId changes
*/
useEffect(() => {
refetch();
}, [userId, refetch]);
// ==============================================================================
// EVENT HANDLERS
// ==============================================================================
/**
* Handle form input changes
*/
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value,
}));
};
/**
* Handle create item form submission
*/
const handleCreateSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validation
if (!formData.name.trim()) {
alert('Name is required');
return;
}
if (!formData.price || parseFloat(formData.price) <= 0) {
alert('Price must be greater than 0');
return;
}
// Submit
createMutation.mutate({
name: formData.name,
description: formData.description,
price: formData.price,
is_active: formData.is_active,
});
};
/**
* Handle edit item button click
*/
const handleEditClick = (item: Item) => {
setEditingItemId(item.id);
setFormData({
name: item.name,
description: item.description || '',
price: item.price.toString(),
is_active: item.is_active,
});
};
/**
* Handle update item form submission
*/
const handleUpdateSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!editingItemId) return;
updateMutation.mutate({
itemId: editingItemId,
data: formData,
});
};
/**
* Handle cancel edit/create
*/
const handleCancel = () => {
setIsCreating(false);
setEditingItemId(null);
setFormData({ name: '', description: '', price: '', is_active: true });
};
/**
* Handle delete item
*/
const handleDelete = (itemId: number) => {
if (window.confirm('Are you sure you want to delete this item?')) {
deleteMutation.mutate(itemId);
}
};
// ==============================================================================
// RENDER HELPERS
// ==============================================================================
/**
* Render loading state
*/
if (isLoading) {
return (
<div className={`flex items-center justify-center p-8 ${className}`}>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading items...</span>
</div>
);
}
/**
* Render error state
*/
if (error) {
return (
<div className={`bg-red-50 border border-red-200 rounded-lg p-6 ${className}`}>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg
className="h-6 w-6 text-red-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading items</h3>
<p className="mt-1 text-sm text-red-700">{(error as Error).message}</p>
<button
onClick={() => refetch()}
className="mt-3 text-sm font-medium text-red-600 hover:text-red-500"
>
Try again
</button>
</div>
</div>
</div>
);
}
/**
* Render empty state
*/
if (!items || items.length === 0) {
return (
<div className={`text-center p-12 bg-gray-50 rounded-lg ${className}`}>
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<h3 className="mt-4 text-sm font-medium text-gray-900">No items found</h3>
<p className="mt-1 text-sm text-gray-500">Get started by creating a new item.</p>
<button
onClick={() => setIsCreating(true)}
className="mt-6 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
Create Item
</button>
</div>
);
}
// ==============================================================================
// MAIN RENDER
// ==============================================================================
return (
<div className={`space-y-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">Items</h2>
{!isCreating && !editingItemId && (
<button
onClick={() => setIsCreating(true)}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
+ Create Item
</button>
)}
</div>
{/* Create/Edit Form */}
{(isCreating || editingItemId) && (
<form
onSubmit={editingItemId ? handleUpdateSubmit : handleCreateSubmit}
className="bg-white shadow rounded-lg p-6 space-y-4"
>
<h3 className="text-lg font-medium text-gray-900">
{editingItemId ? 'Edit Item' : 'Create New Item'}
</h3>
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Name *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description
</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleInputChange}
rows={3}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
/>
</div>
<div>
<label htmlFor="price" className="block text-sm font-medium text-gray-700">
Price *
</label>
<input
type="number"
id="price"
name="price"
value={formData.price}
onChange={handleInputChange}
step="0.01"
min="0.01"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="is_active"
name="is_active"
checked={formData.is_active}
onChange={handleInputChange}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="is_active" className="ml-2 block text-sm text-gray-900">
Active
</label>
</div>
<div className="flex gap-3">
<button
type="submit"
disabled={createMutation.isPending || updateMutation.isPending}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{createMutation.isPending || updateMutation.isPending
? 'Saving...'
: editingItemId
? 'Update Item'
: 'Create Item'}
</button>
<button
type="button"
onClick={handleCancel}
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Cancel
</button>
</div>
{(createMutation.isError || updateMutation.isError) && (
<div className="bg-red-50 border border-red-200 rounded p-3">
<p className="text-red-800 text-sm">
{(createMutation.error || updateMutation.error)?.message}
</p>
</div>
)}
</form>
)}
{/* Items List */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item) => (
<div key={item.id} className="bg-white shadow rounded-lg p-6 space-y-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-lg font-medium text-gray-900">{item.name}</h3>
{item.description && (
<p className="mt-1 text-sm text-gray-500">{item.description}</p>
)}
</div>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
item.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</div>
<div className="text-2xl font-bold text-gray-900">
£{item.price.toFixed(2)}
</div>
<div className="text-xs text-gray-500">
Created {new Date(item.created_at).toLocaleDateString()}
</div>
<div className="flex gap-2 pt-3 border-t">
<button
onClick={() => handleEditClick(item)}
className="flex-1 inline-flex justify-center items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Edit
</button>
<button
onClick={() => handleDelete(item.id)}
disabled={deleteMutation.isPending}
className="flex-1 inline-flex justify-center items-center px-3 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:opacity-50"
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
))}
</div>
</div>
);
};
// ==============================================================================
// HOW TO USE THIS TEMPLATE
// ==============================================================================
/*
1. Copy this file to src/components/YourComponent.tsx
2. Replace "Example" with your domain name:
- ExampleComponent → YourComponent
- Item → YourDataType
3. Update interfaces with your data structure
4. Update API functions with your endpoints
5. Customize the form fields and validation
6. Update styling (currently using Tailwind CSS)
7. Write tests in src/components/__tests__/YourComponent.test.tsx
8. Import and use:
```tsx
import { YourComponent } from './components/YourComponent';
function App() {
return <YourComponent userId={123} />;
}
```
*/