4 min read
React & Next.js Cheatsheet
Core Hooks
jsx// useState
const [count, setCount] = useState(0)
const [user, setUser] = useState<User | null>(null)
setCount(c => c + 1) // functional update
setUser(prev => ({ ...prev, name: 'Alice' })) // partial update
// useEffect
useEffect(() => {
const sub = subscribe(id)
return () => sub.unsubscribe() // cleanup
}, [id]) // re-run when id changes
useEffect(() => { /* once on mount */ }, [])
// useRef — mutable ref, doesn't trigger re-render
const inputRef = useRef<HTMLInputElement>(null)
inputRef.current?.focus()
// useMemo — expensive computation
const sorted = useMemo(() => [...items].sort(compareFn), [items])
// useCallback — stable function reference
const handler = useCallback(() => onSubmit(id), [id, onSubmit])
// useContext
const theme = useContext(ThemeContext)
// useReducer
const [state, dispatch] = useReducer(reducer, initialState)
dispatch({ type: 'INCREMENT', payload: 1 })
// useId — stable unique id (hydration-safe)
const id = useId()
// useDeferredValue — defer expensive render
const deferred = useDeferredValue(searchQuery)
// useTransition — non-urgent state update
const [isPending, startTransition] = useTransition()
startTransition(() => setPage(next))Component Patterns
jsx// Controlled input
function Input({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />
}
// Compound components
function Tabs({ children }) {
const [active, setActive] = useState(0)
return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>
}
Tabs.Panel = function TabPanel({ index, children }) {
const { active } = useContext(TabsContext)
return active === index ? <div>{children}</div> : null
}
// Render prop
function Mouse({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 })
return <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>{render(pos)}</div>
}
// forwardRef
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
))
// Custom hook
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try { return JSON.parse(localStorage.getItem(key) ?? '') } catch { return initial }
})
const set = (v: T) => { setValue(v); localStorage.setItem(key, JSON.stringify(v)) }
return [value, set] as const
}Performance
jsx// Memo — skip re-render when props unchanged
const Card = memo(function Card({ title, body }) {
return <div><h2>{title}</h2><p>{body}</p></div>
})
// lazy + Suspense — code split
const Chart = lazy(() => import('./Chart'))
<Suspense fallback={<Spinner />}><Chart /></Suspense>
// Key — stable keys prevent unnecessary unmount/remount
{items.map(item => <Card key={item.id} {...item} />)}
// Avoid creating objects/functions inline in JSX
// ❌ new object on every render
<Child style={{ color: 'red' }} onClick={() => doThing(id)} />
// ✅ stable references
const style = useMemo(() => ({ color: 'red' }), [])
const handleClick = useCallback(() => doThing(id), [id])
<Child style={style} onClick={handleClick} />Context
tsxinterface ThemeCtx { theme: string; setTheme: (t: string) => void }
const ThemeContext = createContext<ThemeCtx | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light')
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be inside ThemeProvider')
return ctx
}Next.js App Router
app/
layout.tsx — shared UI (not re-rendered on navigation)
page.tsx — route UI
loading.tsx — Suspense boundary
error.tsx — error boundary
not-found.tsx — 404 UI
route.ts — API endpoint
(group)/ — route group (no URL segment)
[param]/ — dynamic segment
[...slug]/ — catch-all
[[...slug]]/ — optional catch-alltsx// Server Component (default) — runs on server, no hooks
export default async function Page({ params, searchParams }) {
const data = await fetch('...') // no useEffect needed
return <div>{data.title}</div>
}
// Client Component
'use client'
export default function Counter() {
const [n, setN] = useState(0)
return <button onClick={() => setN(n + 1)}>{n}</button>
}
// generateStaticParams — pre-render dynamic routes
export async function generateStaticParams() {
return slugs.map(slug => ({ slug }))
}
// generateMetadata
export async function generateMetadata({ params }) {
return { title: params.id, description: '...' }
}
// Route handler (API)
// app/api/users/route.ts
export async function GET(req: Request) {
return Response.json({ users: [] })
}
export async function POST(req: Request) {
const body = await req.json()
return Response.json({ id: 1 }, { status: 201 })
}tsx// Navigation
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import Link from 'next/link'
import { redirect, notFound } from 'next/navigation'
// Image
import Image from 'next/image'
<Image src="/photo.jpg" alt="..." width={800} height={600} priority />
// Server Actions
async function createUser(formData: FormData) {
'use server'
const name = formData.get('name')
await db.createUser({ name })
revalidatePath('/users')
}TypeScript with React
tsx// Component props
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
children: React.ReactNode
className?: string
}
// Event types
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => e.target.value
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() }
const onKeyDown = (e: React.KeyboardEvent) => {}
// Generic component
function List<T extends { id: string }>({ items, render }: {
items: T[]
render: (item: T) => React.ReactNode
}) {
return <ul>{items.map(item => <li key={item.id}>{render(item)}</li>)}</ul>
}
// Hook return types
function useToggle(initial = false): [boolean, () => void] {
const [state, setState] = useState(initial)
return [state, () => setState(s => !s)]
}Common Patterns
jsx// Conditional rendering
{isLoading && <Spinner />}
{error ? <Error msg={error} /> : <Content />}
{items.length === 0 ? <Empty /> : items.map(/* ... */)}
// Portals — render outside parent DOM
createPortal(<Modal />, document.getElementById('modal-root'))
// Error boundary (class component still required)
class ErrorBoundary extends React.Component {
state = { error: null }
static getDerivedStateFromError(e) { return { error: e } }
componentDidCatch(e, info) { console.error(e, info) }
render() {
return this.state.error ? <Fallback /> : this.props.children
}
}
// AbortController in useEffect
useEffect(() => {
const controller = new AbortController()
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== 'AbortError') setError(e) })
return () => controller.abort()
}, [url])[prev·next]