React patterns, hooks, state management, performance optimization, component architecture, TypeScript patterns for components, and data fetching. Absorbs component-patterns, typescript-frontend, typescript-patterns, state-management, zustand, jotai, swr, tanstack-query.
Absorbs
component-patterns
typescript-frontend
typescript-patterns
state-management
zustand
jotai
swr
tanstack-query
Core
React Patterns & Performance
Purpose
Provide expert-level guidance on React component architecture, hook patterns, rendering optimization, and idiomatic React development. This skill covers React 18+ with a focus on concurrent features, Server Components readiness, and production-grade patterns.
Key Patterns
Component Architecture
Compound Components — Use when building flexible, composable UI primitives:
Render Props vs Hooks — Prefer hooks for logic reuse. Use render props only when the consumer needs to control rendering output that depends on the shared state:
// Prefer: Custom hookfunctionuseToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() =>setOn(prev => !prev), []);
const setTrue = useCallback(() =>setOn(true), []);
const setFalse = useCallback(() =>setOn(false), []);
return { on, toggle, setTrue, setFalse } asconst;
}
// Avoid: Render prop for simple logic reuse// Only use render props when children need positional/layout control
Container/Presenter Split — Separate data-fetching and side-effect logic from rendering:
// Container: handles data and effectsfunctionUserProfileContainer({ userId }: { userId: string }) {
const user = use(fetchUser(userId)); // React 19 use()return<UserProfileViewuser={user} />;
}
// Presenter: pure rendering, easy to test and storybookfunctionUserProfileView({ user }: { user: User }) {
return (
<divclassName="p-6 rounded-xl shadow-sm"><h2className="text-xl font-semibold">{user.name}</h2><pclassName="text-base text-gray-600">{user.bio}</p></div>
);
}
Hook Patterns
Custom Hook Rules:
Always prefix with use
Call hooks at the top level only (no conditionals, loops)
Return stable references — wrap callbacks in useCallback, derived objects in useMemo
Document dependency arrays explicitly
useEffect Discipline:
// GOOD: Single responsibility, clear cleanupuseEffect(() => {
const controller = newAbortController();
fetchData(id, { signal: controller.signal })
.then(setData)
.catch(err => {
if (!controller.signal.aborted) setError(err);
});
return() => controller.abort();
}, [id]);
// BAD: Multiple concerns in one effectuseEffect(() => {
fetchData(id).then(setData);
trackPageView(id); // separate effectdocument.title = name; // separate effect
}, [id, name]);
Derived State — Never sync state from props:
// BAD: Syncing state from propsconst [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${first}${last}`);
}, [first, last]);
// GOOD: Derive during renderconst fullName = `${first}${last}`;
// GOOD: Expensive derivationconst sortedItems = useMemo(
() => items.toSorted((a, b) => a.name.localeCompare(b.name)),
[items]
);
Rendering Optimization
When to use React.memo:
Component receives the same props frequently but parent re-renders often
Component is expensive to render (large lists, complex SVG, charts)
Component is a leaf node in a frequently-updating tree
// Wrap with memo when the component is expensive and receives stable-ish propsconstExpensiveList = memo(functionExpensiveList({ items }: { items: Item[] }) {
return (
<ul>
{items.map(item => (
<likey={item.id}>{item.name}</li>
))}
</ul>
);
});
When NOT to use React.memo:
The component is cheap to render
Props change on every render anyway (new objects/arrays inline)
The component already uses context that changes frequently
Avoid Re-render Cascades:
// BAD: New object every render breaks memo
<Child style={{ color: 'red' }} />
// GOOD: Stable referenceconst style = useMemo(() => ({ color: 'red' }), []);
<Childstyle={style} />// BEST: Just use className<ChildclassName="text-red-500" />
Key Prop Strategy:
Use stable, unique IDs from data (never array index for reorderable lists)
Reset component state by changing key: <Form key={formId} />
Design flexible, reusable React components using established patterns — compound components for composable APIs, headless components for logic-only reuse, polymorphic components for element flexibility, and render props for rendering control. Each pattern solves a specific API design problem.
Key Patterns
Pattern Selection Guide
Pattern
Use When
Example
Compound Components
Building composable UI primitives with shared state
Tabs, Accordion, Menu, Select
Headless Components
Reusing logic without prescribing UI
useCombobox, useDialog, useTable
Polymorphic Components
Element type should be consumer-controlled
Button as <a>, Card as <article>
Render Props
Consumer controls what renders based on internal state
Compound components for public APIs — When building a component library, compound components give consumers maximum flexibility without prop explosion.
Hooks over HOCs — Prefer custom hooks for logic reuse. HOCs add wrapper layers and make debugging harder.
Headless for design system foundations — Build headless hooks first (logic + a11y), then wrap with styled components. This separates concerns cleanly.
Support controlled and uncontrolled — Components like inputs, toggles, and accordions should work both ways.
Use context validation — Throw descriptive errors when sub-components are used outside their parent context.
Keep render props for rendering control only — If you are just sharing logic (no rendering control needed), use a hook instead.
Type polymorphic components strictly — Use generics so TypeScript enforces correct props for each as element type.
Use generic PolymorphicProps<E> type for correct prop inference
Render prop callback identity
New function on every render triggers child re-renders
Memoize render prop callbacks or accept stable component references
Headless without ARIA
Logic reused but accessibility missing
Include ARIA attributes in getProps return values
Uncontrolled to controlled switch
React warns about changing from uncontrolled to controlled
Decide mode once based on initial props; document clearly
Over-abstraction
Pattern used where a simple component suffices
Use patterns only when the flexibility is actually needed
From typescript-frontend
TypeScript for frontend development — generics, utility types, type-safe APIs, component typing, and developer experience patterns
TypeScript for Frontend Development
Purpose
Provide expert guidance on TypeScript patterns specific to frontend development — component typing, generic patterns, discriminated unions, type-safe APIs, schema validation inference, and patterns that maximize developer experience (DX) through autocompletion and compile-time safety.
Key Patterns
Component Prop Typing
Basic component with proper HTML attribute forwarding:
readonly arrays in props — Accept readonly T[] to work with both mutable and immutable arrays.
Strict mode always — Enable strict: true in tsconfig. Non-negotiable.
Common Pitfalls
Pitfall
Problem
Fix
as for type assertions
Hides bugs, bypasses checking
Use satisfies, type guards, or Zod parsing
any in event handlers
Loses all type safety
Use React.ChangeEvent<HTMLInputElement> etc.
Enum for options
Runtime overhead, poor tree-shaking
as const arrays with typeof inference
Object or {} types
Too wide, accepts anything
Use Record<string, unknown> or specific shapes
Missing null checks
Runtime errors on optional data
Enable strictNullChecks, use optional chaining
Overtyping
Complex types that obscure intent
Simpler unions, let inference work
Not using readonly
Props can be accidentally mutated
readonly on array/object props
! non-null assertion
Bypasses null safety
Handle null case explicitly
Union type in props without discriminant
Can't narrow which variant
Add a type or kind discriminant field
Ignoring strict tsconfig
Allows any to leak in
strict: true plus noUncheckedIndexedAccess
From typescript-patterns
Advanced TypeScript patterns including branded types, discriminated unions, builder pattern, and type-level programming
TypeScript Advanced Patterns
Purpose
Provide expert guidance on advanced TypeScript type system patterns that enforce correctness at compile time. Covers branded/nominal types for domain primitives, discriminated unions for exhaustive state modeling, the builder pattern for type-safe fluent APIs, and type-level programming with conditional, mapped, and template literal types.
Key Patterns
Branded Types
Use branded types to create nominal distinctions between structurally identical types, preventing accidental misuse of primitive values.
// Define a brand symbol for each domain primitivedeclareconst__brand: unique symbol;
typeBrand<T, B extendsstring> = T & { readonly [__brand]: B };
// Domain primitivestypeUserId = Brand<string, "UserId">;
typeOrderId = Brand<string, "OrderId">;
typeEmail = Brand<string, "Email">;
typePositiveInt = Brand<number, "PositiveInt">;
// Smart constructors with runtime validationfunctionUserId(value: string): UserId {
if (!value.match(/^usr_[a-z0-9]{12}$/)) {
thrownewError(`Invalid UserId: ${value}`);
}
return value asUserId;
}
functionEmail(value: string): Email {
if (!value.includes("@")) {
thrownewError(`Invalid Email: ${value}`);
}
return value asEmail;
}
functionPositiveInt(value: number): PositiveInt {
if (!Number.isInteger(value) || value <= 0) {
thrownewError(`Invalid PositiveInt: ${value}`);
}
return value asPositiveInt;
}
// Compile-time safety: cannot mix branded typesfunctiongetUser(id: UserId): Promise<User> { /* ... */ }
functiongetOrder(id: OrderId): Promise<Order> { /* ... */ }
const userId = UserId("usr_abc123def456");
const orderId = OrderId("ord_xyz789ghi012");
getUser(userId); // OKgetUser(orderId); // Compile error: OrderId is not assignable to UserId
Discriminated Unions
Model exhaustive state machines where the compiler ensures every variant is handled.
// State machine for async data fetchingtypeAsyncState<T, E = Error> =
| { status: "idle" }
| { status: "loading"; startedAt: number }
| { status: "success"; data: T; fetchedAt: number }
| { status: "error"; error: E; retriesLeft: number };
// Exhaustive pattern matching helperfunctionassertNever(value: never): never {
thrownewError(`Unhandled variant: ${JSON.stringify(value)}`);
}
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case"idle":
return"Ready to load";
case"loading":
return`Loading since ${state.startedAt}`;
case"success":
return`Got ${JSON.stringify(state.data)}`;
case"error":
return`Error: ${state.error.message} (${state.retriesLeft} retries left)`;
default:
returnassertNever(state); // Compile error if a variant is missed
}
}
// Domain events as discriminated unionstypeDomainEvent =
| { type: "ORDER_PLACED"; orderId: string; items: Item[]; total: number }
| { type: "ORDER_SHIPPED"; orderId: string; trackingNumber: string }
| { type: "ORDER_CANCELLED"; orderId: string; reason: string }
| { type: "REFUND_ISSUED"; orderId: string; amount: number };
// Extract a specific event typetypeOrderPlacedEvent = Extract<DomainEvent, { type: "ORDER_PLACED" }>;
Builder Pattern
Type-safe builder that tracks which fields have been set at the type level.
Conditional types, mapped types, and template literal types for compile-time computation.
// Deep readonly that works on nested objectstypeDeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extendsobject
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// Path type for deep object access: "user.address.city"typePathKeys<T, Prefixextendsstring = ""> = T extendsobject
? {
[K in keyof T & string]: K | `${K}.${PathKeys<T[K], `${Prefix}${K}.`>}`;
}[keyof T & string]
: never;
typeUser = {
name: string;
address: { city: string; zip: string };
tags: string[];
};
typeUserPaths = PathKeys<User>;
// "name" | "address" | "address.city" | "address.zip" | "tags"// Template literal types for route parameterstypeExtractParams<T extendsstring> =
T extends`${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends`${string}:${infer Param}`
? Param
: never;
typeRouteParams = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"// Mapped type for API response wrapperstypeApiResponse<T> = {
[K in keyof T as`get${Capitalize<string & K>}`]: () => T[K];
} & {
[K in keyof T as`set${Capitalize<string & K>}`]: (value: T[K]) =>void;
};
Best Practices
Prefer branded types over plain primitives for domain values (IDs, emails, currencies) to catch misuse at compile time rather than runtime.
Use discriminated unions over class hierarchies for modeling finite state -- they compose better with pattern matching and type narrowing.
Always include an assertNever default case in switch statements over discriminated unions to catch missing variants after refactoring.
Keep type-level computation shallow -- deeply recursive conditional types slow down the compiler and produce unreadable error messages.
Pair branded types with Zod schemas for runtime validation at system boundaries, keeping the brand as the internal representation.
Common Pitfalls
Pitfall
Problem
Fix
Casting directly to branded type
Bypasses validation, defeats the purpose
Always use a smart constructor function that validates
Missing assertNever in switches
Adding new union variants compiles silently
Add default: return assertNever(x) to every discriminated union switch
Overly deep recursive types
Type instantiation is excessively deep errors, IDE slowdowns
Limit recursion depth with a counter type parameter or flatten the structure
Builder returning this without type narrowing
build() is always callable even when required fields are missing
Use generic state tracking with conditional this parameter on build()
Template literal union explosion
Combining large unions via template literals creates thousands of types
Keep input unions small or use branded string types instead
Forgetting as const on literal objects
TypeScript widens "loading" to string, breaking discrimination
Use as const or explicit type annotations on discriminant values
From state-management
Client-side state management with Zustand, Jotai, Redux Toolkit, and URL state patterns
State Management Skill
Purpose
Select and implement the right state management approach for React applications. This skill covers Zustand for simple-to-medium apps, Jotai for atomic state, Redux Toolkit for large-scale apps, and URL/server state patterns. The key insight: most apps need less state management than developers think.
Key Concepts
State Categories
SERVER STATE (fetched from API):
Use: TanStack Query, SWR, or Next.js server components
NOT Zustand/Redux. Server state belongs in a cache, not a store.
CLIENT STATE (UI interactions):
Local: useState, useReducer (component-scoped)
Shared: Zustand, Jotai (cross-component)
Global: Redux Toolkit (large-scale, complex flows)
URL STATE (route parameters, search params):
Use: nuqs, next/navigation, URLSearchParams
Filters, pagination, sort order belong in the URL, not in a store.
FORM STATE:
Use: React Hook Form, useActionState
Form data belongs in the form library, not in a store.
DECISION TREE:
Is it server data? -> TanStack Query / SWR / RSC
Is it in the URL? -> URL params (nuqs, searchParams)
Is it form data? -> React Hook Form
Is it local UI state? -> useState / useReducer
Is it shared UI state? -> Zustand (simple) or Jotai (atomic)
Is it complex with many actions? -> Redux Toolkit
Library Comparison
ZUSTAND:
Mental model: Top-down store (like Redux, but simpler)
Bundle size: ~1KB
Best for: Shared UI state, simple to medium apps
API: create store -> useStore hook
JOTAI:
Mental model: Bottom-up atoms (like Recoil, but simpler)
Bundle size: ~2KB
Best for: Independent pieces of state, derived state
API: atom() -> useAtom()
REDUX TOOLKIT:
Mental model: Centralized store with slices
Bundle size: ~10KB
Best for: Large apps, complex state machines, middleware
API: createSlice -> configureStore -> useSelector/useDispatch
URL STATE (nuqs):
Mental model: State in the URL, synced with React
Bundle size: ~2KB
Best for: Filters, search, pagination, shareable state
API: useQueryState()
Selectors: Always select specific slices, never useStore() without selector
useShallow: Use for multi-property selections to prevent re-renders
Middleware: persist (localStorage), devtools (Redux DevTools), immer (mutable updates)
Slices pattern: Split large stores into slices combined with ...createSlice()
No providers: Zustand stores work without React context wrappers
Async actions: Just use async/await inside actions, call set() when ready
Subscriptions: useAppStore.subscribe((state) => ...) for side effects outside React
From jotai
Jotai atomic state management for React — primitive atoms, derived atoms, async atoms, atom families, and Suspense integration
Jotai
Layer: domain
Category: state-management
Risk Level: low
Triggers: jotai, atom, useAtom, atomFamily, atomic state
Overview
Jotai is a primitive and flexible state management library for React that takes an atomic approach.
State is built from the bottom up using individual atoms — minimal units of state that compose together.
No boilerplate, no string keys, full TypeScript inference, and React Suspense support out of the box.
When to Use
You need fine-grained reactivity without re-rendering entire subtrees
State is naturally composed from small independent pieces
You want derived/computed state that auto-updates
You need async state that integrates with React Suspense
You want a lightweight alternative to Redux or Zustand with less boilerplate
You need parameterized state (atom families)
Key Patterns
Atom Creation
atom(initialValue) — primitive atom (read-write)
atomWithStorage(key, initialValue) — persisted to localStorage/sessionStorage (jotai/utils)
atomWithDefault(getDefault) — resettable atom with a dynamic default
Derived Atoms
Read-only: atom((get) => get(baseAtom) * 2) — computed from other atoms
React hooks library for data fetching using the stale-while-revalidate HTTP cache strategy. Returns cached (stale) data first, then fetches (revalidates), and finally delivers fresh data. Built-in caching, deduplication, revalidation, and focus tracking.
When to Use
Client-side data fetching in React/Next.js apps
Real-time or near-real-time data that benefits from cache-first rendering
Fetching inside useEffect when SWR handles it — duplicates requests
Using mutable objects as keys — causes infinite revalidation loops
Ignoring isLoading vs isValidating — they indicate different states
Calling mutate() without a key — always scope mutations to a specific key
Nesting SWRConfig without intent — inner config merges with outer
Related Skills
react, nextjs, typescript-frontend, tanstack
From tanstack-query
TanStack Query (React Query) — data fetching, caching, mutations, optimistic updates, infinite scrolling, and prefetching
TanStack Query (React Query) Patterns
Purpose
Provide expert guidance on TanStack Query v5 for React, including data fetching, caching strategies, mutations with optimistic updates, infinite queries, prefetching, SSR hydration with Next.js, and production-grade patterns.