| name | React Frontend Development |
| description | Build React components with TypeScript, Tailwind CSS, and React Query for the Medellin Spark platform. Use when implementing UI features, creating components, managing state, or optimizing frontend performance. Specializes in React hooks, shadcn/ui components, and Supabase integration. |
| version | 1.0.0 |
React Frontend Development
Stack Overview
- Framework: React 18 + TypeScript + Vite
- Styling: Tailwind CSS + shadcn/ui components
- State: React Query (@tanstack/react-query)
- Backend: Supabase (auth, database, edge functions)
- Icons: Lucide React
- Forms: React Hook Form (when needed)
Component Patterns
Standard Component Structure
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
export default function MyComponent() {
const [state, setState] = useState('');
const { toast } = useToast();
const { data, isLoading } = useQuery({
queryKey: ['my-data'],
queryFn: async () => {
const { data, error } = await supabase
.from('table_name')
.select('*');
if (error) throw error;
return data;
},
});
if (isLoading) return <div>Loading...</div>;
return (
<div className="container max-w-4xl mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>
{/* Content here */}
</CardContent>
</Card>
</div>
);
}
React Query Hooks
Data Fetching Pattern
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
export function useMyData() {
return useQuery({
queryKey: ['my-data'],
queryFn: async () => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return [];
const { data, error } = await supabase
.from('table_name')
.select('*')
.eq('profile_id', user.id)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
});
}
export function useCreateData() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: any) => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
const { data, error } = await supabase
.from('table_name')
.insert({ ...payload, profile_id: user.id })
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['my-data'] });
},
});
}
Supabase Integration
Authentication Check
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
Database Queries
const { data, error } = await supabase
.from('table_name')
.select('*')
.eq('status', 'active')
.order('created_at', { ascending: false });
const { data, error } = await supabase
.from('table_name')
.insert({ field: 'value' })
.select()
.single();
const { error } = await supabase
.from('table_name')
.update({ field: 'new_value' })
.eq('id', itemId);
const { error } = await supabase
.from('table_name')
.delete()
.eq('id', itemId);
Tailwind CSS Patterns
Common Layouts
{}
<div className="container max-w-6xl mx-auto py-8">
{}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{}
<div className="flex items-center justify-between gap-4">
{}
<div className="space-y-4">
<Card>...</Card>
<Card>...</Card>
</div>
shadcn/ui Components
Button Variants
<Button>Primary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
Form Components
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input placeholder="Enter name" value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div>
<Label>Description</Label>
<Textarea placeholder="Enter description" />
</div>
<div>
<Label>Category</Label>
<Select value={category} onValueChange={setCategory}>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>
</div>
</div>
Toast Notifications
import { useToast } from '@/hooks/use-toast';
const { toast } = useToast();
toast({
title: 'Success!',
description: 'Item created successfully',
});
toast({
title: 'Error',
description: 'Failed to create item',
variant: 'destructive',
});
Performance Optimization
Lazy Loading
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
Memoization
import { useMemo, useCallback } from 'react';
const expensiveValue = useMemo(() => {
return items.filter(item => item.active).map(item => item.value);
}, [items]);
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
TypeScript Best Practices
Type Interfaces
interface User {
id: string;
email: string;
full_name: string | null;
created_at: string;
}
interface Job {
id: string;
title: string;
company_name: string;
skills: string[];
is_active: boolean;
}
Component Props
interface MyComponentProps {
title: string;
items: Job[];
onSelect?: (id: string) => void;
className?: string;
}
export function MyComponent({ title, items, onSelect, className }: MyComponentProps) {
}
Common Patterns
Loading States
if (isLoading) {
return <div className="flex items-center justify-center py-8">Loading...</div>;
}
if (error) {
return <div className="text-destructive">Error: {error.message}</div>;
}
Empty States
{items.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<p>No items found</p>
</div>
) : (
<div className="grid gap-4">
{items.map(item => <ItemCard key={item.id} item={item} />)}
</div>
)}
Responsive Design
{}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{}
</div>
{}
<div className="hidden md:block">Desktop only content</div>
{}
<div className="md:hidden">Mobile only content</div>
Icons with Lucide
import { Star, Heart, Trash2, Edit, Plus } from 'lucide-react';
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Item
</Button>
<Star className="h-5 w-5 text-yellow-500" />
File Structure
src/
├── components/ # Reusable UI components
│ ├── ui/ # shadcn/ui components (auto-generated)
│ └── *.tsx # Custom components
├── pages/ # Route components
├── hooks/ # Custom React hooks (useMyData, etc.)
├── lib/ # Utilities, helpers
├── types/ # TypeScript types/interfaces
└── integrations/ # Supabase client
Quick Checklist
Before creating a component:
Common Mistakes to Avoid
- Don't use user_id - Always use
profile_id for foreign keys
- Don't skip error handling - Always check
error from Supabase
- Don't forget auth checks - Verify user is authenticated
- Don't hardcode values - Use env variables for URLs/keys
- Don't skip TypeScript - Define proper interfaces
- Don't ignore RLS - Ensure Row Level Security is enabled
Reference