ReactReact Cheatsheet

React Cheatsheet

A dense, copy-paste-ready reference covering every major React concept. Jump to the section you need — each code block is self-contained and immediately usable.

1. Component Basics

Function component

TSX
// Minimal function component
function Greeting() {
  return <h1>Hello, world!</h1>
}

// With props (destructured)
function Welcome({ name, age }: { name: string; age: number }) {
  return <p>{name} is {age} years old.</p>
}

// Default props via default parameter values
function Button({ label = 'Click me', disabled = false }) {
  return <button disabled={disabled}>{label}</button>
}

// Fragment — avoids an extra DOM wrapper
function Pair() {
  return (
    <>
      <p>First</p>
      <p>Second</p>
    </>
  )
}

// children prop
function Card({ children }: { children: React.ReactNode }) {
  return <div className="card">{children}</div>
}

export default Greeting
2. JSX Essentials

JSX quick reference

JSX
// Expressions go in curly braces
const name = 'Alice'
const el = <h1>Hello, {name}!</h1>

// className instead of class
<div className="container active">...</div>

// Inline styles — camelCase keys, value is a string or number
<p style={{ color: 'red', fontSize: 16 }}>Styled text</p>

// Conditional rendering
{isLoggedIn && <Dashboard />}
{isLoggedIn ? <Dashboard /> : <Login />}

// Rendering a list — always provide a stable key
const items = ['Apple', 'Banana', 'Cherry']
<ul>
  {items.map((item) => (
    <li key={item}>{item}</li>
  ))}
</ul>

// Rendering objects — spread props
const inputProps = { type: 'text', placeholder: 'Search…' }
<input {...inputProps} />
Note
JSX compiles to `React.createElement()` calls. Attribute names follow camelCase (`onClick`, `htmlFor`, `tabIndex`). The single exception is `aria-*` and `data-*` — those keep their hyphenated form.
3. Hooks Quick Reference

Hook

Purpose

Signature

useState

Local component state

const [val, setVal] = useState(init)

useEffect

Side effects & subscriptions

useEffect(() => { … }, [deps])

useContext

Read a React context

const val = useContext(MyCtx)

useReducer

Complex state logic

const [state, dispatch] = useReducer(reducer, init)

useRef

Mutable ref / DOM node

const ref = useRef(initVal)

useMemo

Memoize expensive value

const val = useMemo(() => compute(), [deps])

useCallback

Memoize function reference

const fn = useCallback(() => { … }, [deps])

useId

Stable unique ID (a11y)

const id = useId()

useTransition

Mark state update as non-urgent

const [isPending, startTransition] = useTransition()

useDeferredValue

Defer re-render of slow tree

const deferred = useDeferredValue(val)

useLayoutEffect

Sync effect after DOM paint

useLayoutEffect(() => { … }, [deps])

useImperativeHandle

Customize ref exposed to parent

useImperativeHandle(ref, () => ({ … }), [deps])

4. useState Patterns

useState — boolean, object, array

TSX
import { useState } from 'react'

// --- Boolean toggle ---
const [isOpen, setIsOpen] = useState(false)
const toggle = () => setIsOpen((prev) => !prev)

// --- Object state — always spread to preserve other fields ---
const [user, setUser] = useState({ name: '', email: '' })
const updateName = (name: string) =>
  setUser((prev) => ({ ...prev, name }))

// --- Array — add item ---
const [items, setItems] = useState<string[]>([])
const addItem = (item: string) =>
  setItems((prev) => [...prev, item])

// --- Array — remove item by index ---
const removeItem = (index: number) =>
  setItems((prev) => prev.filter((_, i) => i !== index))

// --- Array — update item at index ---
const updateItem = (index: number, next: string) =>
  setItems((prev) =>
    prev.map((item, i) => (i === index ? next : item))
  )

// --- Lazy initial state (expensive computation runs once) ---
const [count, setCount] = useState(() => {
  const saved = localStorage.getItem('count')
  return saved ? Number(saved) : 0
})
Warning
Never mutate state directly. Always produce a new object or array. React uses referential equality to detect changes — mutating in place means React sees the same reference and skips the re-render.
5. useEffect Patterns

useEffect — all common patterns

TSX
import { useEffect, useState } from 'react'

// --- Run once on mount (empty deps array) ---
useEffect(() => {
  document.title = 'App loaded'
}, [])

// --- Run on every render (no deps array at all) ---
useEffect(() => {
  console.log('rendered')
})

// --- Run when specific dep changes ---
useEffect(() => {
  document.title = `Count: ${count}`
}, [count])

// --- Cleanup: returned function runs before next effect & on unmount ---
useEffect(() => {
  const timer = setInterval(() => tick(), 1000)
  return () => clearInterval(timer)
}, [])

// --- Fetch with AbortController (prevents setState after unmount) ---
useEffect(() => {
  const controller = new AbortController()

  async function load() {
    try {
      const res = await fetch('/api/data', { signal: controller.signal })
      const json = await res.json()
      setData(json)
    } catch (err) {
      if ((err as Error).name !== 'AbortError') setError(err as Error)
    }
  }

  load()
  return () => controller.abort()
}, [id]) // re-fetch when id changes

// --- Event listener cleanup ---
useEffect(() => {
  const handleResize = () => setWidth(window.innerWidth)
  window.addEventListener('resize', handleResize)
  return () => window.removeEventListener('resize', handleResize)
}, [])
6. useRef Patterns

useRef — DOM access, mutable value, previous value

TSX
import { useRef, useEffect, useState } from 'react'

// --- DOM ref: focus an input on mount ---
function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    inputRef.current?.focus()
  }, [])

  return <input ref={inputRef} />
}

// --- Mutable value: track interval ID without triggering re-render ---
function Timer() {
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

  const start = () => {
    intervalRef.current = setInterval(() => tick(), 1000)
  }
  const stop = () => {
    if (intervalRef.current) clearInterval(intervalRef.current)
  }
  return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>
}

// --- Previous value pattern ---
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)
7. useReducer Pattern

useReducer — typed reducer

TSX
import { useReducer } from 'react'

type State = { count: number; step: number }
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'setStep'; payload: number }

const initialState: State = { count: 0, step: 1 }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step }
    case 'decrement':
      return { ...state, count: state.count - state.step }
    case 'reset':
      return initialState
    case 'setStep':
      return { ...state, step: action.payload }
    default:
      return state
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <>
      <p>Count: {state.count} (step: {state.step})</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <input
        type="number"
        value={state.step}
        onChange={(e) =>
          dispatch({ type: 'setStep', payload: Number(e.target.value) })
        }
      />
    </>
  )
}
8. Context Pattern

createContext + Provider + useContext

TSX
import { createContext, useContext, useState, ReactNode } from 'react'

// 1. Define the shape
interface ThemeContextValue {
  theme: 'light' | 'dark'
  toggle: () => void
}

// 2. Create context with a sensible default (used when no Provider is above)
const ThemeContext = createContext<ThemeContextValue>({
  theme: 'light',
  toggle: () => {},
})

// 3. Provider component — owns the state
export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  const toggle = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'))

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  )
}

// 4. Custom hook — encapsulates useContext + null guard
export function useTheme() {
  return useContext(ThemeContext)
}

// 5. Consumer
function ThemeToggleButton() {
  const { theme, toggle } = useTheme()
  return (
    <button onClick={toggle}>
      Current theme: {theme}
    </button>
  )
}
9. Custom Hook Template

Custom hook skeleton

TSX
import { useState, useEffect } from 'react'

// Convention: name starts with "use", returns values/actions
function useCustomHook(param: string) {
  const [data, setData] = useState<string | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    let cancelled = false
    setLoading(true)

    someAsyncOperation(param)
      .then((result) => {
        if (!cancelled) setData(result)
      })
      .catch((err) => {
        if (!cancelled) setError(err)
      })
      .finally(() => {
        if (!cancelled) setLoading(false)
      })

    return () => { cancelled = true }
  }, [param])

  return { data, loading, error }
}

useLocalStorage — practical custom hook

TSX
import { useState, useEffect } from 'react'

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue
    try {
      const item = window.localStorage.getItem(key)
      return item ? (JSON.parse(item) as T) : initialValue
    } catch {
      return initialValue
    }
  })

  const setValue = (value: T | ((prev: T) => T)) => {
    try {
      const next = value instanceof Function ? value(storedValue) : value
      setStoredValue(next)
      window.localStorage.setItem(key, JSON.stringify(next))
    } catch (err) {
      console.error(err)
    }
  }

  return [storedValue, setValue] as const
}

// Usage:
// const [name, setName] = useLocalStorage('username', '')
// const [prefs, setPrefs] = useLocalStorage('prefs', { dark: false })
10. React.memo + useMemo + useCallback

Memoization — all three patterns side by side

TSX
import { memo, useMemo, useCallback, useState } from 'react'

// --- React.memo: skip re-render when props haven't changed ---
const ExpensiveChild = memo(function ExpensiveChild({
  value,
  onClick,
}: {
  value: number
  onClick: () => void
}) {
  console.log('ExpensiveChild rendered')
  return <button onClick={onClick}>{value}</button>
})

// --- useMemo: memoize a derived value ---
function Parent({ items }: { items: number[] }) {
  const [filter, setFilter] = useState(0)

  // Only recomputes when items or filter changes
  const filtered = useMemo(
    () => items.filter((n) => n > filter),
    [items, filter]
  )

  // --- useCallback: stable function reference so memo child doesn't re-render ---
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, []) // empty deps → same reference forever

  return (
    <>
      <input
        type="number"
        value={filter}
        onChange={(e) => setFilter(Number(e.target.value))}
      />
      {filtered.map((n) => (
        <ExpensiveChild key={n} value={n} onClick={handleClick} />
      ))}
    </>
  )
}
Tip
Reach for `React.memo` + `useCallback` together — `memo` only prevents re-renders if every prop reference is stable. If you pass an inline function to a memoized child, that new function reference breaks the optimization every render.
11. Event Handlers

Event handler types and patterns

TSX
import { MouseEvent, ChangeEvent, FormEvent, KeyboardEvent } from 'react'

// onClick
function handleClick(e: MouseEvent<HTMLButtonElement>) {
  e.preventDefault()
  console.log('clicked at', e.clientX, e.clientY)
}

// onChange — input
function handleChange(e: ChangeEvent<HTMLInputElement>) {
  setValue(e.target.value)
}

// onSubmit — form
function handleSubmit(e: FormEvent<HTMLFormElement>) {
  e.preventDefault()
  // read form data…
}

// onKeyDown
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
  if (e.key === 'Enter') submit()
  if (e.key === 'Escape') clear()
}

// Passing args to a handler (use a wrapper arrow function)
function ItemList({ items }: { items: string[] }) {
  const handleDelete = (id: string) => () => {
    deleteItem(id)
  }

  return (
    <ul>
      {items.map((item) => (
        <li key={item}>
          {item}
          <button onClick={handleDelete(item)}>Delete</button>
        </li>
      ))}
    </ul>
  )
}
12. Form Patterns

Controlled input, multiple inputs, form submit

TSX
import { useState, ChangeEvent, FormEvent } from 'react'

// --- Single controlled input ---
function SearchBox() {
  const [query, setQuery] = useState('')
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search…"
    />
  )
}

// --- Multiple inputs via name key ---
interface FormValues {
  username: string
  email: string
  age: string
}

function SignupForm() {
  const [form, setForm] = useState<FormValues>({
    username: '',
    email: '',
    age: '',
  })

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target
    setForm((prev) => ({ ...prev, [name]: value }))
  }

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    console.log('submitted', form)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={form.username} onChange={handleChange} placeholder="Username" />
      <input name="email"    value={form.email}    onChange={handleChange} placeholder="Email" />
      <input name="age"      value={form.age}      onChange={handleChange} placeholder="Age" type="number" />
      <button type="submit">Sign up</button>
    </form>
  )
}

// --- Select, checkbox, textarea ---
<select value={selected} onChange={(e) => setSelected(e.target.value)}>
  <option value="a">Option A</option>
  <option value="b">Option B</option>
</select>

<input
  type="checkbox"
  checked={agreed}
  onChange={(e) => setAgreed(e.target.checked)}
/>

<textarea value={bio} onChange={(e) => setBio(e.target.value)} />
13. React Router v6

React Router v6 — setup and core hooks

TSX
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  NavLink,
  Outlet,
  useParams,
  useNavigate,
  useSearchParams,
  useLocation,
} from 'react-router-dom'

// --- App-level setup ---
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users" element={<UsersLayout />}>
          <Route index element={<UserList />} />
          <Route path=":id" element={<UserDetail />} />
        </Route>
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  )
}

// --- Nested layout with Outlet ---
function UsersLayout() {
  return (
    <div>
      <nav>
        <Link to="/users">All Users</Link>
      </nav>
      <Outlet /> {/* child routes render here */}
    </div>
  )
}

// --- useParams ---
function UserDetail() {
  const { id } = useParams<{ id: string }>()
  return <p>User ID: {id}</p>
}

// --- useNavigate ---
function LoginForm() {
  const navigate = useNavigate()
  const handleSubmit = () => {
    // after login…
    navigate('/dashboard', { replace: true })
    // go back: navigate(-1)
  }
  return <button onClick={handleSubmit}>Login</button>
}

// --- useSearchParams (/products?sort=price&order=asc) ---
function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams()
  const sort = searchParams.get('sort') ?? 'name'

  return (
    <button onClick={() => setSearchParams({ sort: 'price' })}>
      Sort by price (current: {sort})
    </button>
  )
}

// --- NavLink — applies activeClassName automatically ---
<NavLink
  to="/about"
  className={({ isActive }) => (isActive ? 'active' : '')}
>
  About
</NavLink>
14. Error Boundary

Error boundary — class component template

TSX
import { Component, ReactNode, ErrorInfo } from 'react'

interface Props {
  fallback?: ReactNode
  children: ReactNode
}

interface State {
  hasError: boolean
  error: Error | null
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    // Log to an error reporting service
    console.error('ErrorBoundary caught:', error, info.componentStack)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div role="alert">
          <h2>Something went wrong.</h2>
          <details>
            <summary>Details</summary>
            <pre>{this.state.error?.message}</pre>
          </details>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try again
          </button>
        </div>
      )
    }
    return this.props.children
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary fallback={<p>Oops! Please reload the page.</p>}>
      <MyWidget />
    </ErrorBoundary>
  )
}
Note
Error boundaries only catch errors during rendering, lifecycle methods, and constructors of child components. They do NOT catch errors in event handlers (use try/catch there), async code, or server-side rendering.
15. TypeScript Props

Typing props — interface, optional, children, events

TSX
import { ReactNode, MouseEvent, ChangeEvent, CSSProperties } from 'react'

// --- Basic interface ---
interface ButtonProps {
  label: string              // required string
  variant?: 'primary' | 'secondary'  // optional union
  disabled?: boolean
  onClick: (e: MouseEvent<HTMLButtonElement>) => void
}

// --- children ---
interface PanelProps {
  title: string
  children: ReactNode        // anything renderable
}

// --- Generic component ---
interface ListProps<T> {
  items: T[]
  renderItem: (item: T, index: number) => ReactNode
  keyExtractor: (item: T) => string
}

function GenericList<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  )
}

// --- Common event types ---
type InputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => void
type SelectChangeHandler = (e: ChangeEvent<HTMLSelectElement>) => void
type TextAreaChangeHandler = (e: ChangeEvent<HTMLTextAreaElement>) => void

// --- forwardRef with TypeScript ---
import { forwardRef } from 'react'

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...rest }, ref) => (
    <label>
      {label}
      <input ref={ref} {...rest} />
    </label>
  )
)
Input.displayName = 'Input'
16. Lazy Loading & Suspense

React.lazy + Suspense + code splitting

TSX
import { lazy, Suspense } from 'react'

// Lazy-load a component (dynamic import)
const Dashboard = lazy(() => import('./Dashboard'))
const Chart     = lazy(() => import('./Chart'))

// Fallback renders while the lazy component loads
function App() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Dashboard />
    </Suspense>
  )
}

// Multiple lazy components share one Suspense boundary
function Analytics() {
  return (
    <Suspense fallback={<Spinner />}>
      <Chart type="bar" />
      <Chart type="line" />
    </Suspense>
  )
}

// Nested Suspense boundaries for granular loading UI
function Page() {
  return (
    <>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>
      <Suspense fallback={<ContentSkeleton />}>
        <MainContent />
      </Suspense>
    </>
  )
}
17. Performance Checklist
  • React.memo — wrap stable child components that receive the same props most renders

  • useCallback — stabilize function props passed to memoized children

  • useMemo — memoize expensive derived values (sorts, filters, transforms)

  • React.lazy + Suspense — split large components out of the initial bundle

  • Virtualize long lists — use react-window or @tanstack/react-virtual for lists > 100 items

  • Key stability — use stable IDs as keys, never array index for reorderable lists

  • Avoid context churn — split context values that change at different frequencies

  • State colocation — keep state as close to where it's used as possible

  • useTransition — wrap non-urgent state updates so they don't block user input

  • Profiler — use React DevTools Profiler to find the actual bottleneck before optimizing

18. Common Patterns

Conditional rendering patterns

TSX
// Short-circuit — renders nothing when condition is false
{isLoggedIn && <UserMenu />}

// Ternary — renders one of two branches
{isLoading ? <Spinner /> : <Content data={data} />}

// Early return null — useful inside a component
function Tooltip({ visible, text }: { visible: boolean; text: string }) {
  if (!visible) return null
  return <div className="tooltip">{text}</div>
}

// iife for complex conditions in JSX
{(() => {
  if (status === 'loading') return <Spinner />
  if (status === 'error')   return <Error />
  return <Data />
})()}

// Switch pattern via object map
const statusMap: Record<string, ReactNode> = {
  loading: <Spinner />,
  error:   <ErrorMessage />,
  success: <DataView />,
}
return statusMap[status] ?? null

List render patterns

TSX
// Basic list
{users.map((user) => (
  <UserCard key={user.id} user={user} />
))}

// Empty state
{users.length === 0
  ? <p>No users found.</p>
  : users.map((user) => <UserCard key={user.id} user={user} />)
}

// Grouped list
{Object.entries(groupedItems).map(([group, items]) => (
  <section key={group}>
    <h3>{group}</h3>
    {items.map((item) => (
      <Item key={item.id} item={item} />
    ))}
  </section>
))}

// Paginated slice
{items.slice(0, page * pageSize).map((item) => (
  <Item key={item.id} item={item} />
))}

Portal — render outside the parent DOM tree

TSX
import { createPortal } from 'react-dom'

function Modal({ children, isOpen }: { children: ReactNode; isOpen: boolean }) {
  if (!isOpen) return null

  // Renders children into <div id="modal-root"> regardless of where
  // Modal sits in the component tree
  return createPortal(
    <div className="modal-overlay">
      <div className="modal-box">{children}</div>
    </div>,
    document.getElementById('modal-root')!
  )
}

// index.html needs: <div id="modal-root"></div>

Render props pattern

TSX
interface MouseTrackerProps {
  render: (pos: { x: number; y: number }) => ReactNode
}

function MouseTracker({ render }: MouseTrackerProps) {
  const [pos, setPos] = useState({ x: 0, y: 0 })

  return (
    <div
      style={{ width: '100%', height: 300, border: '1px solid #ccc' }}
      onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}
    >
      {render(pos)}
    </div>
  )
}

// Usage:
<MouseTracker
  render={({ x, y }) => <p>Mouse at ({x}, {y})</p>}
/>

Compound components pattern

TSX
import { createContext, useContext, useState, ReactNode } from 'react'

// Internal context — not exported
const TabsContext = createContext<{
  active: string
  setActive: (id: string) => void
} | null>(null)

function useTabsContext() {
  const ctx = useContext(TabsContext)
  if (!ctx) throw new Error('Must be used inside <Tabs>')
  return ctx
}

// Parent
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
  const [active, setActive] = useState(defaultTab)
  return (
    <TabsContext.Provider value={{ active, setActive }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  )
}

// Sub-components
function TabList({ children }: { children: ReactNode }) {
  return <div role="tablist">{children}</div>
}

function Tab({ id, children }: { id: string; children: ReactNode }) {
  const { active, setActive } = useTabsContext()
  return (
    <button
      role="tab"
      aria-selected={active === id}
      onClick={() => setActive(id)}
    >
      {children}
    </button>
  )
}

function TabPanel({ id, children }: { id: string; children: ReactNode }) {
  const { active } = useTabsContext()
  return active === id ? <div role="tabpanel">{children}</div> : null
}

// Attach as static properties for ergonomic usage
Tabs.List  = TabList
Tabs.Tab   = Tab
Tabs.Panel = TabPanel

// Usage:
// <Tabs defaultTab="overview">
//   <Tabs.List>
//     <Tabs.Tab id="overview">Overview</Tabs.Tab>
//     <Tabs.Tab id="details">Details</Tabs.Tab>
//   </Tabs.List>
//   <Tabs.Panel id="overview"><p>Overview content</p></Tabs.Panel>
//   <Tabs.Panel id="details"><p>Details content</p></Tabs.Panel>
// </Tabs>
19. useId & useTransition

useId — stable accessible IDs

TSX
import { useId } from 'react'

// Generates a unique, stable ID that is consistent across server and client
// — critical for avoiding hydration mismatches with SSR
function LabeledInput({ label }: { label: string }) {
  const id = useId()
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  )
}

// Multiple IDs from one call using a suffix
function Form() {
  const baseId = useId()
  return (
    <form>
      <label htmlFor={`${baseId}-name`}>Name</label>
      <input id={`${baseId}-name`} />
      <label htmlFor={`${baseId}-email`}>Email</label>
      <input id={`${baseId}-email`} />
    </form>
  )
}

useTransition — keep UI responsive during heavy updates

TSX
import { useState, useTransition } from 'react'

function SearchPage({ allItems }: { allItems: string[] }) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState(allItems)
  const [isPending, startTransition] = useTransition()

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const val = e.target.value
    setQuery(val) // urgent — updates the input immediately

    startTransition(() => {
      // non-urgent — React may defer this if the user is still typing
      setResults(allItems.filter((item) =>
        item.toLowerCase().includes(val.toLowerCase())
      ))
    })
  }

  return (
    <>
      <input value={query} onChange={handleChange} placeholder="Search…" />
      {isPending && <p>Updating results…</p>}
      <ul>
        {results.map((item) => <li key={item}>{item}</li>)}
      </ul>
    </>
  )
}
20. Hook Rules & Anti-patterns

Anti-pattern

Problem

Fix

Calling hooks inside loops / conditions

Hook call order changes between renders → state mismatch

Always call hooks at the top level of the component

Missing dependency in useEffect

Effect reads stale values from a previous render

Add all referenced variables to the deps array; use the eslint-plugin-react-hooks rule

setState during render

Infinite render loop

Move logic to an event handler or useEffect

Mutating state directly (state.x = 1)

React does not detect the change; component does not re-render

Always produce a new object/array with spread or .map()

Using array index as key in a reorderable list

React reuses wrong DOM nodes → broken UI / lost focus

Use a stable, unique ID from the data

Creating objects/arrays inline in render as props to memo children

New reference every render defeats React.memo

Lift the value outside the component or wrap with useMemo

Deriving state in useEffect (setY when x changes)

Causes an extra render; can create infinite loops

Compute derived values during render with useMemo or inline

Tip
Install `eslint-plugin-react-hooks` and enable the `exhaustive-deps` rule. It catches missing dependencies and invalid hook call order at lint time before they cause bugs at runtime.