Software_development_department frontend-patterns
Framework-agnostic React/Vue patterns — component composition, hooks, TanStack Query, memoization, error boundaries. Use for generic React/Vue work (Vite, CRA, Storybook). For Next.js App Router / Server Components specifically, use `senior-frontend` instead.
install
source · Clone the upstream repo
git clone https://github.com/tranhieutt/software_development_department
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/tranhieutt/software_development_department "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/frontend-patterns" ~/.claude/skills/tranhieutt-software-development-department-frontend-patterns && rm -rf "$T"
manifest:
.claude/skills/frontend-patterns/SKILL.mdsource content
Frontend Patterns
Critical rules (non-obvious)
- Stale closure in
: always list all dependencies; useuseEffect
for values that shouldn't trigger re-runuseRef
withuseEffect
: never make the callbackasync
directly — create inner async fn and call itasync- Object/array as dependency: memoize with
/useMemo
or use primitive values; otherwise infinite loopuseCallback - Key prop on lists: use stable IDs, never
when list can reorder or items get deletedindex
is not free: only wrap components with expensive renders and stable prop referencesReact.memo
Component composition patterns
// Compound component with Context const TabsContext = createContext<{ active: string; setActive: (v: string) => void } | null>(null); function Tabs({ children, defaultValue }: { children: React.ReactNode; defaultValue: string }) { const [active, setActive] = useState(defaultValue); return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>; } Tabs.Trigger = function TabsTrigger({ value, children }: { value: string; children: React.ReactNode }) { const ctx = useContext(TabsContext)!; return <button onClick={() => ctx.setActive(value)} aria-selected={ctx.active === value}>{children}</button>; }; Tabs.Content = function TabsContent({ value, children }: { value: string; children: React.ReactNode }) { const { active } = useContext(TabsContext)!; return active === value ? <>{children}</> : null; };
State management decision
| Scope | Solution |
|---|---|
| Single component | , |
| Subtree | Context + |
| Client global (UI) | Zustand / Jotai |
| Server state (API) | TanStack Query |
| Form state | React Hook Form |
| URL state | (Next.js) |
Data fetching with TanStack Query
// Fetch const { data, isLoading, error } = useQuery({ queryKey: ["products", filters], // filters in key → auto-refetch on change queryFn: () => api.getProducts(filters), staleTime: 5 * 60 * 1000, // don't refetch for 5 min }); // Mutate with optimistic update const mutation = useMutation({ mutationFn: api.updateProduct, onMutate: async (newProduct) => { await queryClient.cancelQueries({ queryKey: ["products"] }); const prev = queryClient.getQueryData(["products"]); queryClient.setQueryData(["products"], (old) => old.map(p => p.id === newProduct.id ? newProduct : p)); return { prev }; }, onError: (_, __, ctx) => queryClient.setQueryData(["products"], ctx?.prev), onSettled: () => queryClient.invalidateQueries({ queryKey: ["products"] }), });
Performance: avoid re-renders
// Memoize expensive component const ExpensiveList = memo(({ items }: { items: Item[] }) => ( <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul> )); // Stable callback reference const handleClick = useCallback((id: string) => { onSelect(id); }, [onSelect]); // only recreate if onSelect changes // Expensive calculation const sorted = useMemo(() => items.sort((a, b) => b.score - a.score), [items]);
Code splitting
const HeavyChart = lazy(() => import("./HeavyChart")); function Dashboard() { return ( <Suspense fallback={<Skeleton />}> <HeavyChart data={data} /> </Suspense> ); }
Custom hooks pattern
function useDebounce<T>(value: T, delay: number): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debounced; } function useLocalStorage<T>(key: string, initial: T) { const [value, setValue] = useState<T>(() => { try { return JSON.parse(localStorage.getItem(key) ?? "") ?? initial; } catch { return initial; } }); const set = useCallback((v: T) => { setValue(v); localStorage.setItem(key, JSON.stringify(v)); }, [key]); return [value, set] as const; }
Error boundaries
class ErrorBoundary extends React.Component<{ fallback: React.ReactNode; children: React.ReactNode }> { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: Error) { console.error(error); } render() { return this.state.hasError ? this.props.fallback : this.props.children; } } // Usage: wrap async/complex sections, not entire app
Common pitfalls
| Pitfall | Fix |
|---|---|
Fetching in without cleanup | Use TanStack Query or abort controller |
| Context causes full tree re-render | Split context by domain; memoize value object |
runs twice (StrictMode) | Design effects to be idempotent; use cleanup fn |
| Prop drilling > 3 levels | Lift to Context or state manager |
Missing / states | Always handle all 3 states: loading, error, data |