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.md
source content

Frontend Patterns

Critical rules (non-obvious)

  • Stale closure in
    useEffect
    : always list all dependencies; use
    useRef
    for values that shouldn't trigger re-run
  • useEffect
    with
    async
    : never make the callback
    async
    directly — create inner async fn and call it
  • Object/array as dependency: memoize with
    useMemo
    /
    useCallback
    or use primitive values; otherwise infinite loop
  • Key prop on lists: use stable IDs, never
    index
    when list can reorder or items get deleted
  • React.memo
    is not free
    : only wrap components with expensive renders and stable prop references

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

ScopeSolution
Single component
useState
,
useReducer
SubtreeContext +
useContext
Client global (UI)Zustand / Jotai
Server state (API)TanStack Query
Form stateReact Hook Form
URL state
useSearchParams
(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

PitfallFix
Fetching in
useEffect
without cleanup
Use TanStack Query or abort controller
Context causes full tree re-renderSplit context by domain; memoize value object
useEffect
runs twice (StrictMode)
Design effects to be idempotent; use cleanup fn
Prop drilling > 3 levelsLift to Context or state manager
Missing
loading
/
error
states
Always handle all 3 states: loading, error, data