mirror of
https://github.com/duthaho/claudekit.git
synced 2026-09-03 16:50:51 +03:00
feat: adding new skills, including testing patterns and methodologies, along with bundled resources for better usability.
This commit is contained in:
@@ -1,108 +1,715 @@
|
||||
---
|
||||
name: react
|
||||
description: >
|
||||
Use this skill when building React components, using React hooks, or managing component state. Trigger for any mention of React, JSX, TSX, useState, useEffect, useCallback, useMemo, useContext, custom hooks, or React component patterns. Also applies when implementing context providers, handling component lifecycle, optimizing re-renders, or structuring React application architecture.
|
||||
---
|
||||
|
||||
# React
|
||||
|
||||
## Description
|
||||
|
||||
React component patterns, hooks, and state management best practices.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Building React components
|
||||
- Using React hooks
|
||||
- Component state management
|
||||
- Client-side interactivity in any React-based framework
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- Vue, Svelte, or Angular projects — this skill is React-specific
|
||||
- Backend-only projects without a frontend UI layer
|
||||
- Static HTML pages that do not require a JavaScript framework
|
||||
|
||||
---
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### Functional Components
|
||||
### 1. Hooks
|
||||
|
||||
#### When-to-use guide
|
||||
|
||||
| Hook | Use when you need | Do NOT use for |
|
||||
|------|-------------------|----------------|
|
||||
| `useState` | Simple local state (toggle, form input, counter) | Derived/computed values |
|
||||
| `useEffect` | Side effects: subscriptions, DOM mutations, timers | Data transformation (use useMemo) |
|
||||
| `useRef` | Mutable value that persists across renders without triggering re-render; DOM refs | State that should cause re-render |
|
||||
| `useMemo` | Expensive computation that should only rerun when deps change | Simple/cheap calculations |
|
||||
| `useCallback` | Stable function reference to prevent child re-renders | Every function (only when needed) |
|
||||
| `useReducer` | Complex state with multiple sub-values or state transitions | Simple boolean/string state |
|
||||
| `useContext` | Reading context values | Frequently changing global state (causes re-renders) |
|
||||
|
||||
#### useState
|
||||
|
||||
```tsx
|
||||
interface UserCardProps {
|
||||
user: User;
|
||||
onSelect?: (user: User) => void;
|
||||
// Simple state
|
||||
const [count, setCount] = useState(0);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
// Functional updates (when new state depends on previous)
|
||||
setCount((prev) => prev + 1);
|
||||
|
||||
// Lazy initialization (expensive initial value)
|
||||
const [data, setData] = useState(() => computeExpensiveDefault());
|
||||
```
|
||||
|
||||
#### useEffect
|
||||
|
||||
```tsx
|
||||
// Run on mount + cleanup on unmount
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetchData(controller.signal).then(setData);
|
||||
return () => controller.abort(); // Cleanup
|
||||
}, []); // Empty deps = run once
|
||||
|
||||
// Run when dependency changes
|
||||
useEffect(() => {
|
||||
const handler = () => setWidth(window.innerWidth);
|
||||
window.addEventListener("resize", handler);
|
||||
return () => window.removeEventListener("resize", handler);
|
||||
}, []); // No deps needed — handler is stable
|
||||
|
||||
// Sync external system with state
|
||||
useEffect(() => {
|
||||
document.title = `${count} items`;
|
||||
}, [count]);
|
||||
```
|
||||
|
||||
#### useRef
|
||||
|
||||
```tsx
|
||||
// DOM reference
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const focusInput = () => inputRef.current?.focus();
|
||||
|
||||
// Mutable value (no re-render on change)
|
||||
const renderCount = useRef(0);
|
||||
useEffect(() => {
|
||||
renderCount.current += 1;
|
||||
});
|
||||
|
||||
// Previous value pattern
|
||||
const prevValueRef = useRef(value);
|
||||
useEffect(() => {
|
||||
prevValueRef.current = value;
|
||||
}, [value]);
|
||||
```
|
||||
|
||||
#### useReducer
|
||||
|
||||
```tsx
|
||||
interface State {
|
||||
items: Item[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function UserCard({ user, onSelect }: UserCardProps) {
|
||||
type Action =
|
||||
| { type: "FETCH_START" }
|
||||
| { type: "FETCH_SUCCESS"; payload: Item[] }
|
||||
| { type: "FETCH_ERROR"; error: string };
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case "FETCH_START":
|
||||
return { ...state, loading: true, error: null };
|
||||
case "FETCH_SUCCESS":
|
||||
return { items: action.payload, loading: false, error: null };
|
||||
case "FETCH_ERROR":
|
||||
return { ...state, loading: false, error: action.error };
|
||||
}
|
||||
}
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
items: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Dispatch actions
|
||||
dispatch({ type: "FETCH_START" });
|
||||
```
|
||||
|
||||
### 2. Custom Hooks
|
||||
|
||||
#### Extraction pattern
|
||||
|
||||
Extract a custom hook when:
|
||||
- Two or more components share the same stateful logic
|
||||
- A component's hook logic is complex enough to deserve its own name and tests
|
||||
- You want to abstract away an external API (localStorage, WebSocket, etc.)
|
||||
|
||||
**Rules:**
|
||||
- Name must start with `use`
|
||||
- Can call other hooks (unlike regular functions)
|
||||
- Each call gets its own independent state
|
||||
|
||||
#### Practical examples
|
||||
|
||||
```tsx
|
||||
// useLocalStorage — persist state to localStorage
|
||||
function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
const [stored, setStored] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? (JSON.parse(item) as T) : initialValue;
|
||||
} catch {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = useCallback(
|
||||
(value: T | ((prev: T) => T)) => {
|
||||
setStored((prev) => {
|
||||
const next = value instanceof Function ? value(prev) : value;
|
||||
window.localStorage.setItem(key, JSON.stringify(next));
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[key],
|
||||
);
|
||||
|
||||
return [stored, setValue] as const;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const [theme, setTheme] = useLocalStorage("theme", "light");
|
||||
```
|
||||
|
||||
```tsx
|
||||
// useDebounce — debounce a rapidly changing value
|
||||
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;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
useEffect(() => {
|
||||
fetchResults(debouncedSearch);
|
||||
}, [debouncedSearch]);
|
||||
```
|
||||
|
||||
```tsx
|
||||
// useFetch — generic data fetching hook
|
||||
function useFetch<T>(url: string) {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetch(url, { signal: controller.signal })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => setData(json as T))
|
||||
.catch((err) => {
|
||||
if (err.name !== "AbortError") setError(err);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
return () => controller.abort();
|
||||
}, [url]);
|
||||
|
||||
return { data, error, loading };
|
||||
}
|
||||
|
||||
// Usage
|
||||
const { data: users, loading, error } = useFetch<User[]>("/api/users");
|
||||
```
|
||||
|
||||
### 3. Component Patterns
|
||||
|
||||
#### Compound components
|
||||
|
||||
```tsx
|
||||
// Components that work together, sharing implicit state
|
||||
interface TabsContextType {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
const TabsContext = createContext<TabsContextType | null>(null);
|
||||
|
||||
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
|
||||
const [activeTab, setActiveTab] = useState(defaultTab);
|
||||
return (
|
||||
<div className="card" onClick={() => onSelect?.(user)}>
|
||||
<h3>{user.name}</h3>
|
||||
<p>{user.email}</p>
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||
<div role="tablist">{children}</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function TabTrigger({ value, children }: { value: string; children: ReactNode }) {
|
||||
const ctx = useContext(TabsContext)!;
|
||||
return (
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={ctx.activeTab === value}
|
||||
onClick={() => ctx.setActiveTab(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function TabContent({ value, children }: { value: string; children: ReactNode }) {
|
||||
const ctx = useContext(TabsContext)!;
|
||||
if (ctx.activeTab !== value) return null;
|
||||
return <div role="tabpanel">{children}</div>;
|
||||
}
|
||||
|
||||
// Attach sub-components
|
||||
Tabs.Trigger = TabTrigger;
|
||||
Tabs.Content = TabContent;
|
||||
|
||||
// Usage
|
||||
<Tabs defaultTab="settings">
|
||||
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
||||
<Tabs.Content value="profile"><ProfileForm /></Tabs.Content>
|
||||
<Tabs.Content value="settings"><SettingsForm /></Tabs.Content>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
#### Render props
|
||||
|
||||
```tsx
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function MouseTracker({ render }: { render: (pos: MousePosition) => ReactNode }) {
|
||||
const [pos, setPos] = useState<MousePosition>({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
|
||||
window.addEventListener("mousemove", handler);
|
||||
return () => window.removeEventListener("mousemove", handler);
|
||||
}, []);
|
||||
|
||||
return <>{render(pos)}</>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<MouseTracker render={({ x, y }) => <span>Mouse: {x}, {y}</span>} />
|
||||
```
|
||||
|
||||
#### Controlled vs uncontrolled
|
||||
|
||||
```tsx
|
||||
// Controlled — parent owns the state
|
||||
interface ControlledInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function ControlledInput({ value, onChange }: ControlledInputProps) {
|
||||
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
|
||||
}
|
||||
|
||||
// Uncontrolled — component owns the state, parent reads via ref or callback
|
||||
function UncontrolledInput({ defaultValue }: { defaultValue?: string }) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return <input ref={ref} defaultValue={defaultValue} />;
|
||||
}
|
||||
|
||||
// Flexible pattern — supports both controlled and uncontrolled
|
||||
function FlexibleInput({
|
||||
value: controlledValue,
|
||||
defaultValue = "",
|
||||
onChange,
|
||||
}: {
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}) {
|
||||
const [internalValue, setInternalValue] = useState(defaultValue);
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const value = isControlled ? controlledValue : internalValue;
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (!isControlled) setInternalValue(e.target.value);
|
||||
onChange?.(e.target.value);
|
||||
}
|
||||
|
||||
return <input value={value} onChange={handleChange} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Context
|
||||
|
||||
#### Provider pattern with separate state and dispatch
|
||||
|
||||
```tsx
|
||||
// Split context to prevent unnecessary re-renders
|
||||
interface AppState {
|
||||
user: User | null;
|
||||
theme: "light" | "dark";
|
||||
}
|
||||
|
||||
type AppAction =
|
||||
| { type: "SET_USER"; user: User | null }
|
||||
| { type: "TOGGLE_THEME" };
|
||||
|
||||
const AppStateContext = createContext<AppState | null>(null);
|
||||
const AppDispatchContext = createContext<React.Dispatch<AppAction> | null>(null);
|
||||
|
||||
function appReducer(state: AppState, action: AppAction): AppState {
|
||||
switch (action.type) {
|
||||
case "SET_USER":
|
||||
return { ...state, user: action.user };
|
||||
case "TOGGLE_THEME":
|
||||
return { ...state, theme: state.theme === "light" ? "dark" : "light" };
|
||||
}
|
||||
}
|
||||
|
||||
function AppProvider({ children }: { children: ReactNode }) {
|
||||
const [state, dispatch] = useReducer(appReducer, {
|
||||
user: null,
|
||||
theme: "light",
|
||||
});
|
||||
|
||||
return (
|
||||
<AppStateContext.Provider value={state}>
|
||||
<AppDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</AppDispatchContext.Provider>
|
||||
</AppStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Typed hooks for consumers
|
||||
function useAppState() {
|
||||
const ctx = useContext(AppStateContext);
|
||||
if (!ctx) throw new Error("useAppState must be used within AppProvider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function useAppDispatch() {
|
||||
const ctx = useContext(AppDispatchContext);
|
||||
if (!ctx) throw new Error("useAppDispatch must be used within AppProvider");
|
||||
return ctx;
|
||||
}
|
||||
```
|
||||
|
||||
**Why split?** Components that only dispatch actions (buttons) do not re-render when state changes. Only components that read state re-render.
|
||||
|
||||
#### Context splitting for performance
|
||||
|
||||
```tsx
|
||||
// Instead of one giant context with everything:
|
||||
const UserContext = createContext<User | null>(null);
|
||||
const ThemeContext = createContext<"light" | "dark">("light");
|
||||
const NotificationContext = createContext<Notification[]>([]);
|
||||
|
||||
// Components subscribe only to the context they need
|
||||
function Avatar() {
|
||||
const user = useContext(UserContext); // Only re-renders when user changes
|
||||
return <img src={user?.avatar} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Error Boundaries
|
||||
|
||||
#### Class-based error boundary
|
||||
|
||||
```tsx
|
||||
class ErrorBoundary extends React.Component<
|
||||
{ children: ReactNode; fallback: ReactNode },
|
||||
{ hasError: boolean; error: Error | null }
|
||||
> {
|
||||
constructor(props: { children: ReactNode; fallback: ReactNode }) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
console.error("Error boundary caught:", error, info.componentStack);
|
||||
// Send to error tracking service
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### react-error-boundary library (recommended)
|
||||
|
||||
```tsx
|
||||
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
|
||||
|
||||
function ErrorFallback({
|
||||
error,
|
||||
resetErrorBoundary,
|
||||
}: {
|
||||
error: Error;
|
||||
resetErrorBoundary: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div role="alert">
|
||||
<h2>Something went wrong</h2>
|
||||
<pre>{error.message}</pre>
|
||||
<button onClick={resetErrorBoundary}>Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorFallback}
|
||||
onReset={() => {
|
||||
// Reset app state if needed
|
||||
}}
|
||||
resetKeys={[userId]} // Auto-reset when these values change
|
||||
>
|
||||
<Dashboard />
|
||||
</ErrorBoundary>
|
||||
|
||||
// Programmatic error throwing from child
|
||||
function SaveButton() {
|
||||
const { showBoundary } = useErrorBoundary();
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await saveData();
|
||||
} catch (error) {
|
||||
showBoundary(error); // Propagate to nearest ErrorBoundary
|
||||
}
|
||||
}
|
||||
|
||||
return <button onClick={handleSave}>Save</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Suspense
|
||||
|
||||
#### Suspense boundaries with lazy loading
|
||||
|
||||
```tsx
|
||||
import { Suspense, lazy } from "react";
|
||||
|
||||
// Code-split heavy components
|
||||
const HeavyChart = lazy(() => import("./heavy-chart"));
|
||||
const AdminPanel = lazy(() => import("./admin-panel"));
|
||||
|
||||
function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<Suspense fallback={<ChartSkeleton />}>
|
||||
<HeavyChart />
|
||||
</Suspense>
|
||||
<Suspense fallback={<div>Loading admin panel...</div>}>
|
||||
<AdminPanel />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Hooks
|
||||
#### Suspense with data fetching (React 19+ / framework integration)
|
||||
|
||||
```tsx
|
||||
// useState
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// useEffect
|
||||
useEffect(() => {
|
||||
const subscription = subscribe();
|
||||
return () => subscription.unsubscribe();
|
||||
}, [dependency]);
|
||||
|
||||
// useMemo
|
||||
const expensive = useMemo(() => compute(data), [data]);
|
||||
|
||||
// useCallback
|
||||
const handleClick = useCallback(() => {
|
||||
doSomething(id);
|
||||
}, [id]);
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
```tsx
|
||||
function useUser(id: string) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchUser(id)
|
||||
.then(setUser)
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
return { user, loading };
|
||||
}
|
||||
```
|
||||
|
||||
### Context Pattern
|
||||
|
||||
```tsx
|
||||
const UserContext = createContext<User | null>(null);
|
||||
|
||||
export function UserProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
// With a Suspense-compatible data source (React Query, Next.js, Relay)
|
||||
function ProjectList() {
|
||||
return (
|
||||
<UserContext.Provider value={user}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
<Suspense fallback={<ProjectListSkeleton />}>
|
||||
<ProjectListContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
const context = useContext(UserContext);
|
||||
if (!context) throw new Error('useUser must be within UserProvider');
|
||||
return context;
|
||||
// The data-fetching component suspends while loading
|
||||
function ProjectListContent() {
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: fetchProjects,
|
||||
});
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{data.map((p) => (
|
||||
<li key={p.id}>{p.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Named exports with lazy
|
||||
|
||||
```tsx
|
||||
// For named exports, wrap in a default export adapter
|
||||
const UserSettings = lazy(() =>
|
||||
import("./user-settings").then((mod) => ({ default: mod.UserSettings })),
|
||||
);
|
||||
```
|
||||
|
||||
### 7. Performance
|
||||
|
||||
#### React.memo
|
||||
|
||||
```tsx
|
||||
// Only re-renders when props change (shallow comparison)
|
||||
const ExpensiveList = React.memo(function ExpensiveList({
|
||||
items,
|
||||
onSelect,
|
||||
}: {
|
||||
items: Item[];
|
||||
onSelect: (item: Item) => void;
|
||||
}) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<li key={item.id} onClick={() => onSelect(item)}>
|
||||
{item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
});
|
||||
|
||||
// Custom comparison
|
||||
const Chart = React.memo(ChartComponent, (prev, next) => {
|
||||
return prev.data.length === next.data.length && prev.title === next.title;
|
||||
});
|
||||
```
|
||||
|
||||
#### useMemo and useCallback together
|
||||
|
||||
```tsx
|
||||
function ParentComponent({ items }: { items: Item[] }) {
|
||||
// Memoize expensive derived data
|
||||
const sortedItems = useMemo(
|
||||
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[items],
|
||||
);
|
||||
|
||||
// Stable function reference for memoized child
|
||||
const handleSelect = useCallback((item: Item) => {
|
||||
console.log("Selected:", item.id);
|
||||
}, []);
|
||||
|
||||
// ExpensiveList only re-renders when sortedItems or handleSelect change
|
||||
return <ExpensiveList items={sortedItems} onSelect={handleSelect} />;
|
||||
}
|
||||
```
|
||||
|
||||
#### Key prop optimization
|
||||
|
||||
```tsx
|
||||
// BAD: using index as key — breaks state when list order changes
|
||||
{items.map((item, index) => <Item key={index} data={item} />)}
|
||||
|
||||
// GOOD: stable unique key
|
||||
{items.map((item) => <Item key={item.id} data={item} />)}
|
||||
|
||||
// Force remount: change key to reset component state
|
||||
<ProfileForm key={userId} userId={userId} />
|
||||
// When userId changes, the form unmounts and remounts with fresh state
|
||||
```
|
||||
|
||||
#### Virtualization for large lists
|
||||
|
||||
```tsx
|
||||
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, // Estimated row height in px
|
||||
overscan: 5, // Extra rows rendered above/below viewport
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={parentRef} style={{ height: "400px", overflow: "auto" }}>
|
||||
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
height: `${virtualRow.size}px`,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{items[virtualRow.index].name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Keep components small and focused
|
||||
2. Use TypeScript for props
|
||||
3. Memoize expensive computations
|
||||
4. Clean up effects properly
|
||||
5. Lift state up when needed
|
||||
1. **Keep components small and single-purpose** — if a component exceeds ~100 lines or handles multiple concerns, extract sub-components or custom hooks. Name components after what they render, not what they do.
|
||||
|
||||
2. **Use TypeScript interfaces for all props** — define explicit prop types. Avoid `any`. Use discriminated unions for props that change based on a variant. Export prop types for reuse.
|
||||
|
||||
3. **Clean up all effects** — return a cleanup function from every `useEffect` that subscribes to events, starts timers, or creates abort controllers. Missing cleanups cause memory leaks and stale state bugs.
|
||||
|
||||
4. **Derive state instead of syncing it** — if a value can be computed from props or other state, compute it during render (or with `useMemo`). Never `useEffect` to sync derived state — it causes an extra render.
|
||||
|
||||
5. **Lift state to the lowest common ancestor** — not higher. State should live in the closest parent that needs it. If siblings need shared state, lift to their parent. If distant components need it, use context.
|
||||
|
||||
6. **Use `useCallback` and `React.memo` together, not alone** — `useCallback` only helps when the function is passed to a memoized child. `React.memo` only helps when the parent actually passes stable props. Using one without the other is wasted effort.
|
||||
|
||||
7. **Prefer composition over prop drilling** — instead of passing props through 5 levels, restructure so the parent renders the child directly (component composition) or use context for truly global state.
|
||||
|
||||
8. **Handle all async states** — every data-fetching component should handle loading, error, and empty states. Use Suspense and Error Boundaries for declarative handling. Never leave a component that shows nothing while loading.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Missing dependencies in hooks**: Include all dependencies
|
||||
- **State updates on unmounted**: Use cleanup functions
|
||||
- **Prop drilling**: Use context or composition
|
||||
1. **Missing or wrong dependency arrays** — forgetting to add a dependency to `useEffect`/`useMemo`/`useCallback` causes stale closures. Adding too many causes unnecessary re-runs. Use the `react-hooks/exhaustive-deps` ESLint rule.
|
||||
|
||||
2. **Setting state during render** — calling `setState` unconditionally in the render body causes infinite re-render loops. State updates should be in event handlers, effects, or callbacks — never at the top level of the component function.
|
||||
|
||||
3. **Prop drilling through many layers** — passing a prop through 4+ intermediate components that do not use it. Fix with composition (restructuring the component tree), context (for shared state), or a state management library.
|
||||
|
||||
4. **Creating objects/arrays in JSX props** — `<Child style={{ color: "red" }} />` creates a new object every render, defeating `React.memo`. Hoist constants outside the component or use `useMemo`.
|
||||
|
||||
5. **Using `useEffect` for derived state** — syncing state with `useEffect(() => setFullName(first + last), [first, last])` causes double renders. Just compute it: `const fullName = first + last`. Use `useMemo` if the computation is expensive.
|
||||
|
||||
6. **Not handling race conditions in effects** — when a component fetches data based on a prop, fast prop changes can cause older responses to arrive after newer ones, displaying stale data. Use `AbortController` or a boolean flag to ignore stale responses.
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `frameworks/nextjs` — Next.js App Router, SSR, and full-stack React patterns
|
||||
- `languages/typescript` — TypeScript strict mode and type patterns
|
||||
- `frontend/tailwind` — Styling with Tailwind CSS
|
||||
- `frontend/shadcn-ui` — UI component library built on Radix and Tailwind
|
||||
- `testing/vitest` — Testing React components with vitest and testing-library
|
||||
- `patterns/state-management` — State management patterns for React
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# React Patterns Quick Reference
|
||||
|
||||
## Hook Rules
|
||||
|
||||
1. Only call hooks at the **top level** (not inside loops, conditions, or nested functions)
|
||||
2. Only call hooks from **React function components** or **custom hooks**
|
||||
3. Custom hooks **must** start with `use`
|
||||
4. Hook call order must be **identical** on every render
|
||||
|
||||
```typescript
|
||||
// BAD
|
||||
if (isLoggedIn) {
|
||||
const [user, setUser] = useState(null); // Conditional hook call
|
||||
}
|
||||
|
||||
// GOOD
|
||||
const [user, setUser] = useState(null);
|
||||
// Use the state conditionally instead
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Hook Recipes
|
||||
|
||||
### useLocalStorage
|
||||
|
||||
```typescript
|
||||
function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}, [key, value]);
|
||||
|
||||
return [value, setValue] as const;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const [theme, setTheme] = useLocalStorage("theme", "dark");
|
||||
```
|
||||
|
||||
### useDebounce
|
||||
|
||||
```typescript
|
||||
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;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
|
||||
useEffect(() => {
|
||||
fetchResults(debouncedSearch);
|
||||
}, [debouncedSearch]);
|
||||
```
|
||||
|
||||
### useMediaQuery
|
||||
|
||||
```typescript
|
||||
function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(
|
||||
() => window.matchMedia(query).matches
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(query);
|
||||
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
```
|
||||
|
||||
### usePrevious
|
||||
|
||||
```typescript
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const prevCount = usePrevious(count);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compound Component Pattern
|
||||
|
||||
```typescript
|
||||
// Components that work together sharing implicit state
|
||||
const Accordion = ({ children }: { children: React.ReactNode }) => {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
|
||||
<div role="tablist">{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const AccordionItem = ({ index, title, children }: {
|
||||
index: number; title: string; children: React.ReactNode;
|
||||
}) => {
|
||||
const { openIndex, setOpenIndex } = useContext(AccordionContext);
|
||||
const isOpen = openIndex === index;
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setOpenIndex(isOpen ? null : index)}>
|
||||
{title}
|
||||
</button>
|
||||
{isOpen && <div>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Accordion.Item = AccordionItem;
|
||||
|
||||
// Usage - clean, declarative API
|
||||
<Accordion>
|
||||
<Accordion.Item index={0} title="Section 1">Content 1</Accordion.Item>
|
||||
<Accordion.Item index={1} title="Section 2">Content 2</Accordion.Item>
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Patterns
|
||||
|
||||
### Split State and Dispatch
|
||||
|
||||
```typescript
|
||||
// Prevent unnecessary re-renders by splitting contexts
|
||||
const StateContext = createContext<State | null>(null);
|
||||
const DispatchContext = createContext<Dispatch | null>(null);
|
||||
|
||||
function Provider({ children }: { children: React.ReactNode }) {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<StateContext.Provider value={state}>
|
||||
{children}
|
||||
</StateContext.Provider>
|
||||
</DispatchContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Components that only dispatch won't re-render on state changes
|
||||
function useAppDispatch() {
|
||||
const ctx = useContext(DispatchContext);
|
||||
if (!ctx) throw new Error("Must be used within Provider");
|
||||
return ctx;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
### When to Memoize
|
||||
|
||||
| Situation | Solution |
|
||||
|-----------|----------|
|
||||
| Expensive computation | `useMemo(() => compute(data), [data])` |
|
||||
| Stable callback for child | `useCallback(fn, [deps])` |
|
||||
| Prevent child re-render | `React.memo(Component)` |
|
||||
| Stable object/array prop | `useMemo(() => ({ ... }), [deps])` |
|
||||
|
||||
### When NOT to Memoize
|
||||
|
||||
- Simple/cheap calculations
|
||||
- Primitives (strings, numbers, booleans) -- already stable
|
||||
- Functions only used in the same component
|
||||
- Components that are cheap to render
|
||||
|
||||
### Key Optimizations
|
||||
|
||||
```typescript
|
||||
// 1. Move state down (colocate state with where it's used)
|
||||
// BAD: parent re-renders everything
|
||||
function Parent() {
|
||||
const [hover, setHover] = useState(false);
|
||||
return <><ExpensiveChild /><HoverTarget onHover={setHover} /></>;
|
||||
}
|
||||
|
||||
// GOOD: isolate the stateful part
|
||||
function Parent() {
|
||||
return <><ExpensiveChild /><HoverSection /></>;
|
||||
}
|
||||
function HoverSection() {
|
||||
const [hover, setHover] = useState(false);
|
||||
return <HoverTarget onHover={setHover} />;
|
||||
}
|
||||
|
||||
// 2. Pass children to avoid re-rendering them
|
||||
function ScrollTracker({ children }: { children: React.ReactNode }) {
|
||||
const [scroll, setScroll] = useState(0);
|
||||
// children are created by parent, not re-created on scroll change
|
||||
return <div onScroll={e => setScroll(e.currentTarget.scrollTop)}>{children}</div>;
|
||||
}
|
||||
|
||||
// 3. Use key to reset component state
|
||||
<Form key={selectedId} initialData={data} />
|
||||
|
||||
// 4. Lazy load heavy components
|
||||
const HeavyChart = lazy(() => import("./HeavyChart"));
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<HeavyChart data={data} />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### React DevTools Profiler Checklist
|
||||
|
||||
1. Enable "Highlight updates" to spot unnecessary re-renders
|
||||
2. Record a profiler session and look for:
|
||||
- Components re-rendering without visible changes
|
||||
- Long render times (>16ms blocks frames)
|
||||
- Cascading re-renders from context changes
|
||||
3. Fix with: state colocation, memo, context splitting, or external state
|
||||
Reference in New Issue
Block a user