Skip to content
Back to skills

Frontend Patterns

ASecurity

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.

  • 71 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
data-aibashreactvuenextjsnodeapifrontendperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add tranhieutt/software_development_department --skill frontend-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Frontend Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Frontend Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tranhieutt-frontend-patterns/badge)](https://www.skillsdirectory.com/skills/tranhieutt-frontend-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: frontend-patterns
type: reference
description: "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."
paths: ["**/*.tsx", "**/*.jsx", "**/*.vue", "**/components/**"]
effort: 3
allowed-tools: Read, Glob, Grep, Write, Edit, Bash
user-invocable: true
when_to_use: "When building React/Vue components, hooks, state management, or client-side performance — NOT for Next.js App Router (see `senior-frontend`)"
---

# 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

```tsx
// 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 | `useState`, `useReducer` |
| Subtree | Context + `useContext` |
| Client global (UI) | Zustand / Jotai |
| Server state (API) | TanStack Query |
| Form state | React Hook Form |
| URL state | `useSearchParams` (Next.js) |

## Data fetching with TanStack Query

```tsx
// 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

```tsx
// 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

```tsx
const HeavyChart = lazy(() => import("./HeavyChart"));

function Dashboard() {
  return (
    <Suspense fallback={<Skeleton />}>
      <HeavyChart data={data} />
    </Suspense>
  );
}
```

## Custom hooks pattern

```tsx
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

```tsx
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 `useEffect` without cleanup | Use TanStack Query or abort controller |
| Context causes full tree re-render | Split context by domain; memoize value object |
| `useEffect` runs twice (StrictMode) | Design effects to be idempotent; use cleanup fn |
| Prop drilling > 3 levels | Lift to Context or state manager |
| Missing `loading` / `error` states | Always handle all 3 states: loading, error, data |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.