Skip to main content
Glama
FRONTEND_MANAGER.md15.6 kB
# Frontend Manager Reference Guide **Comprehensive reference for frontend development patterns, best practices, and common pitfalls.** --- ## Core Principles 1. **Component-First**: Build reusable, composable components 2. **Accessibility-First**: WCAG 2.1 AA as minimum standard 3. **Performance-First**: Core Web Vitals as targets 4. **Type-Safety**: TypeScript for all components 5. **Test-Driven**: Components should be testable --- ## React Patterns ### Component Structure ```typescript // components/Button/Button.tsx import { forwardRef, type ButtonHTMLAttributes } from 'react'; import { cn } from '@/lib/utils'; interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'primary' | 'secondary' | 'ghost'; size?: 'sm' | 'md' | 'lg'; loading?: boolean; } export const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant = 'primary', size = 'md', loading, children, disabled, ...props }, ref) => { return ( <button ref={ref} className={cn( 'inline-flex items-center justify-center rounded-md font-medium transition-colors', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', 'disabled:pointer-events-none disabled:opacity-50', { 'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary', 'bg-gray-100 text-gray-900 hover:bg-gray-200': variant === 'secondary', 'hover:bg-gray-100': variant === 'ghost', 'h-8 px-3 text-sm': size === 'sm', 'h-10 px-4': size === 'md', 'h-12 px-6 text-lg': size === 'lg', }, className )} disabled={disabled || loading} {...props} > {loading && <Spinner className="mr-2 h-4 w-4" />} {children} </button> ); } ); Button.displayName = 'Button'; ``` ### Hook Patterns ```typescript // hooks/useAsync.ts import { useState, useCallback, useEffect } from 'react'; interface UseAsyncState<T> { data: T | null; loading: boolean; error: Error | null; } interface UseAsyncResult<T> extends UseAsyncState<T> { execute: () => Promise<void>; reset: () => void; } export function useAsync<T>( asyncFunction: () => Promise<T>, immediate = true ): UseAsyncResult<T> { const [state, setState] = useState<UseAsyncState<T>>({ data: null, loading: immediate, error: null, }); const execute = useCallback(async () => { setState({ data: null, loading: true, error: null }); try { const data = await asyncFunction(); setState({ data, loading: false, error: null }); } catch (error) { setState({ data: null, loading: false, error: error as Error }); } }, [asyncFunction]); const reset = useCallback(() => { setState({ data: null, loading: false, error: null }); }, []); useEffect(() => { if (immediate) { execute(); } }, [execute, immediate]); return { ...state, execute, reset }; } ``` ### Context Pattern ```typescript // context/AuthContext.tsx import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; interface User { id: string; email: string; name: string; } interface AuthContextValue { user: User | null; loading: boolean; login: (email: string, password: string) => Promise<void>; logout: () => Promise<void>; } const AuthContext = createContext<AuthContextValue | null>(null); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); const login = useCallback(async (email: string, password: string) => { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!response.ok) throw new Error('Login failed'); const { user } = await response.json(); setUser(user); }, []); const logout = useCallback(async () => { await fetch('/api/auth/logout', { method: 'POST' }); setUser(null); }, []); return ( <AuthContext.Provider value={{ user, loading, login, logout }}> {children} </AuthContext.Provider> ); } export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within AuthProvider'); } return context; } ``` --- ## State Management ### When to Use What | Approach | Use For | Examples | |----------|---------|----------| | `useState` | Local UI state | Form inputs, toggles | | `useReducer` | Complex local state | Multi-field forms | | Context | Theme, auth, i18n | Shared across tree | | Zustand/Redux | Global app state | User data, cart | | React Query | Server state | API data with caching | ### Zustand Store ```typescript // stores/cartStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface CartItem { id: string; name: string; price: number; quantity: number; } interface CartStore { items: CartItem[]; addItem: (item: Omit<CartItem, 'quantity'>) => void; removeItem: (id: string) => void; updateQuantity: (id: string, quantity: number) => void; clearCart: () => void; total: () => number; } export const useCartStore = create<CartStore>()( persist( (set, get) => ({ items: [], addItem: (item) => set((state) => { const existing = state.items.find((i) => i.id === item.id); if (existing) { return { items: state.items.map((i) => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i ), }; } return { items: [...state.items, { ...item, quantity: 1 }] }; }), removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id), })), updateQuantity: (id, quantity) => set((state) => ({ items: state.items.map((i) => i.id === id ? { ...i, quantity } : i ), })), clearCart: () => set({ items: [] }), total: () => get().items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ), }), { name: 'cart-storage' } ) ); ``` ### React Query ```typescript // hooks/useUsers.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; interface User { id: string; name: string; email: string; } async function fetchUsers(): Promise<User[]> { const res = await fetch('/api/users'); if (!res.ok) throw new Error('Failed to fetch users'); return res.json(); } async function createUser(data: Omit<User, 'id'>): Promise<User> { const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) throw new Error('Failed to create user'); return res.json(); } export function useUsers() { return useQuery({ queryKey: ['users'], queryFn: fetchUsers, staleTime: 5 * 60 * 1000, // 5 minutes }); } export function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: createUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); }, }); } ``` --- ## Form Handling ### React Hook Form + Zod ```typescript // components/LoginForm.tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const loginSchema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Minimum 8 characters'), }); type LoginFormData = z.infer<typeof loginSchema>; interface LoginFormProps { onSubmit: (data: LoginFormData) => Promise<void>; } export function LoginForm({ onSubmit }: LoginFormProps) { const { register, handleSubmit, formState: { errors, isSubmitting }, setError, } = useForm<LoginFormData>({ resolver: zodResolver(loginSchema), }); const handleFormSubmit = async (data: LoginFormData) => { try { await onSubmit(data); } catch (error) { setError('root', { message: 'Invalid credentials' }); } }; return ( <form onSubmit={handleSubmit(handleFormSubmit)} noValidate> <div className="space-y-4"> <div> <label htmlFor="email" className="block text-sm font-medium"> Email </label> <input id="email" type="email" {...register('email')} className="mt-1 block w-full rounded-md border p-2" aria-invalid={!!errors.email} aria-describedby={errors.email ? 'email-error' : undefined} /> {errors.email && ( <p id="email-error" className="mt-1 text-sm text-red-600" role="alert"> {errors.email.message} </p> )} </div> <div> <label htmlFor="password" className="block text-sm font-medium"> Password </label> <input id="password" type="password" {...register('password')} className="mt-1 block w-full rounded-md border p-2" aria-invalid={!!errors.password} /> {errors.password && ( <p className="mt-1 text-sm text-red-600" role="alert"> {errors.password.message} </p> )} </div> {errors.root && ( <p className="text-sm text-red-600" role="alert"> {errors.root.message} </p> )} <button type="submit" disabled={isSubmitting} className="w-full rounded-md bg-blue-600 py-2 text-white disabled:opacity-50" > {isSubmitting ? 'Logging in...' : 'Log In'} </button> </div> </form> ); } ``` --- ## Accessibility Checklist ### ARIA Requirements ```tsx // Modal example <div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-description" > <h2 id="modal-title">Confirm Action</h2> <p id="modal-description">Are you sure you want to proceed?</p> </div> // Button states <button aria-pressed={isActive} aria-disabled={isLoading} aria-busy={isLoading} > {isLoading ? 'Loading...' : 'Submit'} </button> // Form errors <input aria-invalid={!!error} aria-describedby={error ? 'error-message' : undefined} /> <p id="error-message" role="alert">{error}</p> // Live regions for dynamic content <div aria-live="polite" aria-atomic="true"> {notification} </div> ``` ### Keyboard Navigation ```typescript // Keyboard handler for custom components function handleKeyDown(event: React.KeyboardEvent) { switch (event.key) { case 'Enter': case ' ': event.preventDefault(); handleClick(); break; case 'Escape': handleClose(); break; case 'ArrowDown': event.preventDefault(); focusNext(); break; case 'ArrowUp': event.preventDefault(); focusPrevious(); break; } } ``` ### Focus Management ```typescript // Focus trap for modals import { useRef, useEffect } from 'react'; function useFocusTrap(isActive: boolean) { const containerRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!isActive || !containerRef.current) return; const container = containerRef.current; const focusableElements = container.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0] as HTMLElement; const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; firstElement?.focus(); function handleKeyDown(e: KeyboardEvent) { if (e.key !== 'Tab') return; if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault(); lastElement?.focus(); } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault(); firstElement?.focus(); } } container.addEventListener('keydown', handleKeyDown); return () => container.removeEventListener('keydown', handleKeyDown); }, [isActive]); return containerRef; } ``` --- ## Performance Patterns ### Code Splitting ```typescript // Route-based splitting const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); // Component-based splitting const HeavyEditor = lazy(() => import('./components/HeavyEditor')); // Preload on hover const Analytics = lazy(() => import('./pages/Analytics')); const preloadAnalytics = () => import('./pages/Analytics'); <Link to="/analytics" onMouseEnter={preloadAnalytics}> Analytics </Link> ``` ### Memoization Guidelines ```typescript // DO memoize: // - Expensive calculations const sortedItems = useMemo(() => items.sort((a, b) => a.name.localeCompare(b.name)), [items] ); // - Callbacks passed to memoized children const handleClick = useCallback(() => { doSomething(id); }, [id]); // - Components with expensive rendering const ExpensiveChart = memo(function Chart({ data }) { return <canvas>{/* complex rendering */}</canvas>; }); // DON'T memoize: // - Simple calculations // - Callbacks for native elements // - Components that re-render anyway ``` --- ## Testing Patterns ### Component Testing ```typescript import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from './LoginForm'; describe('LoginForm', () => { it('submits with valid data', async () => { const user = userEvent.setup(); const onSubmit = jest.fn(); render(<LoginForm onSubmit={onSubmit} />); await user.type(screen.getByLabelText(/email/i), 'test@example.com'); await user.type(screen.getByLabelText(/password/i), 'password123'); await user.click(screen.getByRole('button', { name: /log in/i })); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123', }); }); }); it('shows validation errors', async () => { const user = userEvent.setup(); render(<LoginForm onSubmit={jest.fn()} />); await user.click(screen.getByRole('button', { name: /log in/i })); expect(await screen.findByText(/invalid email/i)).toBeInTheDocument(); }); }); ``` --- ## Common Pitfalls | Pitfall | Symptom | Solution | |---------|---------|----------| | Stale closures | Wrong values in callbacks | Add deps to useCallback | | Missing keys | React warnings, bugs | Use unique, stable keys | | Prop drilling | Deeply nested props | Use context or state lib | | Hydration mismatch | SSR warnings | Use useEffect for client | | Memory leaks | Performance issues | Cleanup in useEffect | | Over-rendering | Slow UI | Profile and memoize | --- ## Quick Reference ```typescript // Utility function for className merging import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // Common hooks const [state, setState] = useState(initial); const ref = useRef<HTMLElement>(null); useEffect(() => { /* effect */ return () => { /* cleanup */ }; }, [deps]); const memoized = useMemo(() => expensive(), [deps]); const callback = useCallback(() => {}, [deps]); const context = useContext(MyContext); ```

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/RhizomaticRobin/cerebras-code-fullstack-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server