// ❌ WRONG — conditional hook
if (isLoggedIn) {
const [user, setUser] = useState(null);
}
// ✅ CORRECT — always call, conditionally use
const [user, setUser] = useState(null);
if (!isLoggedIn) return null;
// Mount only
useEffect(() => { initAnalytics(); }, []);
// Sync with dependency
useEffect(() => { document.title = Count: ${count}; }, [count]);
// Cleanup (unmount + re-run)
useEffect(() => {
const controller = new AbortController();
fetch(/api/data, { signal: controller.signal })
.then(r => r.json()).then(setData);
return () => controller.abort();
}, [query]);
// Subscription pattern
useEffect(() => {
const sub = eventBus.subscribe('refresh', handleRefresh);
return () => sub.unsubscribe();
}, [handleRefresh]);
const initialState = { items: [], loading: false, error: null };
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START': return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS': return { ...state, loading: false, items: action.payload };
case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload };
case 'ADD_ITEM': return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM': return { ...state, items: state.items.filter(i => i.id !== action.payload) };
default: return state;
}
}
function ItemList() {
const [state, dispatch] = useReducer(reducer, initialState);
}
// Memoize expensive computation
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.priority - b.priority), [items]
);
// Memoize callback reference for child component
const handleClick = useCallback((id) => { setSelected(id); }, []);
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function useAsync(asyncFn, deps = []) {
const [state, dispatch] = useReducer(
(s, a) => ({ ...s, ...a }), { data: null, loading: true, error: null }
);
useEffect(() => {
let cancelled = false;
dispatch({ loading: true, error: null });
asyncFn()
.then(data => { if (!cancelled) dispatch({ data, loading: false }); })
.catch(err => { if (!cancelled) dispatch({ error: err, loading: false }); });
return () => { cancelled = true; };
}, deps);
return state;
}
graph TD
A[State Need] --> B{Scope?}
B -->|Component-local| C[useState / useReducer]
B -->|Shared across subtree| D[Context API]
B -->|Global + frequent updates| E{Complexity?}
E -->|Medium| F[Zustand / Jotai]
E -->|High + middleware| G[Redux Toolkit]
A --> H{Server data?}
H -->|Yes| I[TanStack Query]
H -->|No| B
const ThemeCtx = createContext({ theme: 'light', toggle: () => {} });
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggle = useCallback(() => setTheme(t => t === 'light' ? 'dark' : 'light'), []);
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return {children} ;
}
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useStore = create(devtools(persist((set, get) => ({
cart: [],
addToCart: (item) => set(s => ({ cart: [...s.cart, item] })),
removeFromCart: (id) => set(s => ({ cart: s.cart.filter(i => i.id !== id) })),
total: () => get().cart.reduce((sum, i) => sum + i.price, 0),
}), { name: 'cart-store' })));
// Selector prevents unnecessary re-renders
const items = useStore(s => s.cart);
import { atom, useAtom, useAtomValue } from 'jotai';
const userAtom = atom(null);
const isLoggedInAtom = atom((get) => get(userAtom) !== null);
function Profile() {
const [user, setUser] = useAtom(userAtom);
const loggedIn = useAtomValue(isLoggedInAtom);
if (!loggedIn) return ;
return
{user.name};
}
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit';
const fetchUsers = createAsyncThunk('users/fetch', async () => {
const res = await fetch('/api/users'); return res.json();
});
const usersSlice = createSlice({
name: 'users',
initialState: { list: [], status: 'idle', error: null },
reducers: { addUser: (s, a) => { s.list.push(a.payload); } },
extraReducers: (b) => {
b.addCase(fetchUsers.pending, s => { s.status = 'loading'; })
.addCase(fetchUsers.fulfilled, (s, a) => { s.list = a.payload; s.status = 'succeeded'; })
.addCase(fetchUsers.rejected, (s, a) => { s.status = 'failed'; s.error = a.error.message; });
},
});
const store = configureStore({ reducer: { users: usersSlice.reducer } });
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60_000, retry: 2,
});
}
function useAddUser() {
const qc = useQueryClient();
return useMutation({
mutationFn: (u) => fetch('/api/users', { method: 'POST', body: JSON.stringify(u) }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['users'] }),
});
}
graph TD
App --> ThemeProvider
ThemeProvider --> Layout
Layout --> Header
Layout --> ErrorBoundary
ErrorBoundary --> PageContent
PageContent --> DataProvider
DataProvider --> List
List --> ListItem
ListItem --> WithTooltip
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
const ctx = useMemo(() => ({ activeIndex, setActiveIndex }), [activeIndex]);
return {children} ;
}
Tabs.List = function TabList({ children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
return (
{Children.map(children, (child, i) =>
cloneElement(child, { isActive: i === activeIndex, onClick: () => setActiveIndex(i) })
)}
);
};
Tabs.Panel = function TabPanel({ children, index }) {
const { activeIndex } = useContext(TabsContext);
return activeIndex === index ?
{children} : null;
};
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return render(pos);
}
// Usage: } />
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return ;
if (!user) return ;
return ;
};
}
// Usage: export default withAuth(Dashboard);
// Controlled — React owns the truth
const [value, setValue] = useState('');
setValue(e.target.value)} />;
// Uncontrolled — DOM owns the truth
const ref = useRef();
;
// Read: ref.current.value
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) { return { hasError: true, error }; }
componentDidCatch(error, info) { logErrorToService(error, info.componentStack); }
render() {
return this.state.hasError
? (this.props.fallback ?? Something went wrong.
)
: this.props.children;
}
}
JSX auto-escapes {userInput}. The real risk is dangerouslySetInnerHTML and href injection:
// ❌ VULNERABLE
Click
// ✅ SANITIZE
import DOMPurify from 'dompurify';
// ✅ VALIDATE PROTOCOL
const SAFE_URL = /^(https?:|mailto:|tel:)/i;
function SafeLink({ href, ...props }) {
if (!SAFE_URL.test(href)) return Invalid Link;
return ;
}
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];
function safeMerge(target, source) {
const clean = Object.entries(source)
.filter(([k]) => !DANGEROUS_KEYS.includes(k))
.reduce((o, [k, v]) => ({ ...o, [k]: v }), {});
return { ...target, ...clean };
}
function useAuth() {
const [token, setToken] = useState(() => localStorage.getItem('token'));
const login = async (creds) => {
const { token } = await fetch('/api/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(creds),
}).then(r => r.json());
localStorage.setItem('token', token);
setToken(token);
};
const logout = () => { localStorage.removeItem('token'); setToken(null); };
return { token, login, logout, isAuthenticated: !!token };
}
// Axios interceptor for CSRF
axios.interceptors.request.use((config) => {
if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
config.headers['X-CSRF-Token'] = getCSRFToken();
}
return config;
});
sequenceDiagram
participant Parent
participant Child
participant DOM
Parent->>Parent: setState / dispatch
Parent->>Child: Re-render (props change?)
Note over Child: React.memo checks shallow equality
alt Props unchanged
Child-->>Parent: Skip render (memo hit)
else Props changed
Child->>Child: Render VDOM
Child->>DOM: Commit minimal diff
end
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
return items.map(item => );
}, (prev, next) => prev.items === next.items && prev.onSelect === next.onSelect);
const AdminPanel = React.lazy(() => import('./AdminPanel'));
function App() {
return (
}>
} />
);
}
// Data fetching with Suspense
function Profile() {
const { data } = useSuspenseQuery({ queryKey: ['profile'], queryFn: fetchProfile });
return
{data.name};
}
describe('formatCurrency', () => {
it('formats USD', () => expect(formatCurrency(1234.5)).toBe('$1,234.50'));
it('handles zero', () => expect(formatCurrency(0)).toBe('$0.00'));
it('handles negatives', () => expect(formatCurrency(-10)).toBe('-$10.00'));
});
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('submits credentials and shows success', async () => {
const onSuccess = jest.fn();
render( );
const user = userEvent.setup();
await user.type(screen.getByLabelText(/email/i), 'user@test.com');
await user.type(screen.getByLabelText(/password/i), 'secret123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => expect(onSuccess).toHaveBeenCalledWith({
email: 'user@test.com', password: 'secret123',
}));
});
test('user can log in and see dashboard', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'admin@test.com');
await page.fill('[data-testid="password"]', 'admin123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome, Admin');
});
| Topic | Key API | When to Use |
|-------|---------|-------------|
| Local state | useState / useReducer | Component-scoped |
| Shared subtree | Context API | Theme, locale, auth |
| Global store | Zustand / Jotai | Medium-complexity global |
| Enterprise store | Redux Toolkit | Middleware, devtools, large teams |
| Server cache | TanStack Query | API data, pagination, mutations |
| Memoization | useMemo / useCallback | Expensive calc or stable ref |
| Compound comps | Context + cloneElement | API |
| Error handling | ErrorBoundary | Catch render crashes |
| Security | DOMPurify, CSRF tokens | User HTML, state-changing requests |
| Perf | React.memo, lazy, Suspense | Prevent re-renders, reduce bundle |
useRef for mutable latest valueuseMemokey → use stable unique IDs.catch() or error boundarydangerouslySetInnerHTML without sanitization → always use DOMPurify