Frontend Patterns Skill
Modern React patterns and best practices for building scalable applications.
Component Patterns
- Compound Components
// Parent provides context, children consume it const Tabs = ({ children, defaultTab }) => { const [activeTab, setActiveTab] = useState(defaultTab);
return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> <div className="tabs">{children}</div> </TabsContext.Provider> ); };
Tabs.List = ({ children }) => <div className="tabs-list">{children}</div>; Tabs.Tab = ({ id, children }) => { const { activeTab, setActiveTab } = useTabsContext(); return ( <button className={activeTab === id ? 'active' : ''} onClick={() => setActiveTab(id)} > {children} </button> ); }; Tabs.Panel = ({ id, children }) => { const { activeTab } = useTabsContext(); return activeTab === id ? <div>{children}</div> : null; };
// Usage <Tabs defaultTab="tab1"> <Tabs.List> <Tabs.Tab id="tab1">Tab 1</Tabs.Tab> <Tabs.Tab id="tab2">Tab 2</Tabs.Tab> </Tabs.List> <Tabs.Panel id="tab1">Content 1</Tabs.Panel> <Tabs.Panel id="tab2">Content 2</Tabs.Panel> </Tabs>
- Render Props
interface MousePosition { x: number; y: number; }
const MouseTracker = ({ render, }: { render: (pos: MousePosition) => ReactNode; }) => { const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => { const handleMove = (e: MouseEvent) => { setPosition({ x: e.clientX, y: e.clientY }); }; window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []);
return <>{render(position)}</>; };
// Usage <MouseTracker render={({ x, y }) => ( <div>Mouse: {x}, {y}</div> )} />
- Custom Hooks
// Reusable logic extraction function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } });
const setValue = (value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } };
return [storedValue, setValue] as const; }
- Higher-Order Components (HOC)
function withAuth<P extends object>(Component: ComponentType<P>) { return function AuthenticatedComponent(props: P) { const { user, isLoading } = useAuth();
if (isLoading) return <Spinner />;
if (!user) return <Navigate to="/login" />;
return <Component {...props} />;
}; }
// Usage const ProtectedDashboard = withAuth(Dashboard);
State Management Patterns
Zustand Store Pattern
interface Store { // State items: Item[]; isLoading: boolean;
// Actions fetchItems: () => Promise<void>; addItem: (item: Item) => void; removeItem: (id: string) => void; }
const useStore = create<Store>((set, get) => ({ items: [], isLoading: false,
fetchItems: async () => { set({ isLoading: true }); try { const items = await api.getItems(); set({ items, isLoading: false }); } catch { set({ isLoading: false }); } },
addItem: (item) => set((state) => ({ items: [...state.items, item], })),
removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id), })), }));
// Selectors for performance const useItems = () => useStore((s) => s.items); const useIsLoading = () => useStore((s) => s.isLoading);
React Query Pattern
// Query keys factory const todoKeys = { all: ['todos'] as const, lists: () => [...todoKeys.all, 'list'] as const, list: (filters: string) => [...todoKeys.lists(), { filters }] as const, details: () => [...todoKeys.all, 'detail'] as const, detail: (id: string) => [...todoKeys.details(), id] as const, };
// Queries export const useTodos = (filters?: string) => useQuery({ queryKey: todoKeys.list(filters ?? ''), queryFn: () => fetchTodos(filters), });
// Mutations with optimistic updates export const useUpdateTodo = () => { const queryClient = useQueryClient();
return useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: todoKeys.lists() }); const previous = queryClient.getQueryData(todoKeys.lists()); queryClient.setQueryData(todoKeys.lists(), (old) => old?.map((t) => (t.id === newTodo.id ? newTodo : t)) ); return { previous }; }, onError: (err, newTodo, context) => { queryClient.setQueryData(todoKeys.lists(), context?.previous); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: todoKeys.lists() }); }, }); };
Performance Patterns
Memoization
// Memoize expensive calculations const expensiveValue = useMemo(() => { return items.filter(x => x.active).reduce((acc, x) => acc + x.value, 0); }, [items]);
// Memoize callbacks const handleClick = useCallback((id: string) => { setSelected(id); }, []);
// Memoize components const MemoizedList = memo(({ items }: { items: Item[] }) => ( <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> ));
Code Splitting
// Lazy load routes const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings'));
// With loading fallback <Suspense fallback={<PageSkeleton />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense>
Virtual Lists
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) { const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, });
return ( <div ref={parentRef} style={{ height: 400, overflow: 'auto' }}> <div style={{ height: virtualizer.getTotalSize() }}> {virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key} style={{ position: 'absolute', top: virtualItem.start, height: virtualItem.size, }} > {items[virtualItem.index].name} </div> ))} </div> </div> ); }
Error Handling
Error Boundaries
class ErrorBoundary extends Component<Props, State> { state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) { return { hasError: true, error }; }
componentDidCatch(error: Error, info: ErrorInfo) { console.error('Error caught:', error, info); }
render() { if (this.state.hasError) { return <ErrorFallback error={this.state.error} />; } return this.props.children; } }
Accessibility Patterns
// Focus management const modalRef = useRef<HTMLDivElement>(null); useEffect(() => { modalRef.current?.focus(); }, []);
// ARIA attributes <button aria-label="Close modal" aria-expanded={isOpen} aria-controls="modal-content"
<CloseIcon /> </button>
// Keyboard navigation const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowDown': setFocusedIndex(i => Math.min(i + 1, items.length - 1)); break; case 'ArrowUp': setFocusedIndex(i => Math.max(i - 1, 0)); break; case 'Enter': handleSelect(focusedIndex); break; } };