React
Targeting React 19
Syntax
Component
interface Props {
name: string
age?: number
}
function Greeting({ name, age = 0 }: Props) {
return <p>Hi {name}, age {age}</p>
}Children
interface Props {
children: React.ReactNode
}
function Card({ children }: Props) {
return <div className="card">{children}</div>
}
// usage
<Card><p>Hello</p></Card>Fragment
// return multiple elements without a wrapper
function List() {
return (
<>
<li>One</li>
<li>Two</li>
</>
)
}Conditional Render
function Status({ ok }: { ok: boolean }) {
return (
<div>
{ok ? <p>Success</p> : <p>Error</p>}
{ok && <span>All good</span>}
</div>
)
}List Render
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
)
}Event Handler
function Button() {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
console.log('clicked')
}
return <button onClick={handleClick}>Go</button>
}Style & Class
// inline style — object with camelCase keys
<div style={{ color: 'red', fontSize: 14 }} />
// className — not class
<div className="card active" />
// conditional classes
<div className={`tab ${active ? 'selected' : ''}`} />Spread Props
interface BtnProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'ghost'
}
function Btn({ variant = 'primary', ...rest }: BtnProps) {
return <button className={variant} {...rest} />
}
// passes onClick, disabled, etc. throughBeginner Patterns
useState
const [count, setCount] = useState(0)
setCount(5) // set directly
setCount((prev) => prev + 1) // updater function
// typed state
const [user, setUser] = useState<User | null>(null)useEffect
// run on mount
useEffect(() => {
fetchData()
}, []) // empty deps = mount only
// run when dep changes
useEffect(() => {
fetchUser(id)
}, [id]) // re-runs when id changes
// cleanup
useEffect(() => {
const sub = subscribe()
return () => sub.unsubscribe() // cleanup on unmount
}, [])useRef
// DOM reference
const inputRef = useRef<HTMLInputElement>(null)
inputRef.current?.focus()
// mutable value that doesn't trigger re-render
const renderCount = useRef(0)
renderCount.current++
// in JSX
<input ref={inputRef} />Form Input
const [name, setName] = useState('')
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>Lifting State
// parent owns the state, passes down
function Parent() {
const [value, setValue] = useState('')
return (
<>
<Input value={value} onChange={setValue} />
<Display text={value} />
</>
)
}
function Input({ value, onChange }: {
value: string
onChange: (v: string) => void
}) {
return <input value={value}
onChange={(e) => onChange(e.target.value)} />
}Fetch on Mount
const [data, setData] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/posts')
.then((res) => res.json())
.then((posts) => setData(posts))
.finally(() => setLoading(false))
}, [])
if (loading) return <p>Loading...</p>Intermediate Patterns
useCallback
// memoize a function so it keeps the same reference
const handleDelete = useCallback((id: number) => {
setItems((prev) => prev.filter((i) => i.id !== id))
}, [])
// without useCallback, a new function is created every render
// pass to memoized children to prevent unnecessary re-rendersuseMemo
// memoize an expensive computation
const sorted = useMemo(
() => items.toSorted((a, b) => a.price - b.price),
[items] // recompute only when items changes
)
// don't overuse — only for genuinely expensive workContext
interface ThemeCtx { theme: string; toggle: () => void }
const ThemeContext = createContext<ThemeCtx | null>(null)
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('dark')
const toggle = () => setTheme((t) => t === 'dark' ? 'light' : 'dark')
return (
<ThemeContext value={{ theme, toggle }}>
{children}
</ThemeContext>
)
}
// consume
const ctx = useContext(ThemeContext)useReducer
type Action =
| { type: 'inc' }
| { type: 'dec' }
| { type: 'set'; value: number }
function reducer(state: number, action: Action) {
switch (action.type) {
case 'inc': return state + 1
case 'dec': return state - 1
case 'set': return action.value
}
}
const [count, dispatch] = useReducer(reducer, 0)
dispatch({ type: 'inc' })Custom Hook
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initial
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}
const [theme, setTheme] = useLocalStorage('theme', 'dark')Portal
import { createPortal } from 'react-dom'
function Modal({ children }: { children: React.ReactNode }) {
return createPortal(
<div className="modal-overlay">
<div className="modal">{children}</div>
</div>,
document.body // render outside the component tree
)
}Advanced Patterns
forwardRef
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => (
<label>
{label}
<input ref={ref} {...props} />
</label>
)
)
// parent can ref the inner <input>
const ref = useRef<HTMLInputElement>(null)
<Input ref={ref} label="Name" />Generic Component
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>
}
<List
items={users}
renderItem={(u) => <li key={u.id}>{u.name}</li>}
/>Compound Component
function Tabs({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState(0)
return (
<TabsContext value={{ active, setActive }}>
{children}
</TabsContext>
)
}
Tabs.Tab = function Tab({ index, children }: {
index: number; children: React.ReactNode
}) {
const { active, setActive } = useContext(TabsContext)!
return (
<button
className={active === index ? 'active' : ''}
onClick={() => setActive(index)}
>{children}</button>
)
}Suspense + Lazy
import { lazy, Suspense } from 'react'
const Settings = lazy(() => import('./Settings'))
function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Settings />
</Suspense>
)
}
// Settings chunk loads only when renderedError Boundary
import { ErrorBoundary } from 'react-error-boundary'
function Fallback({ error }: { error: Error }) {
return <p>Something broke: {error.message}</p>
}
<ErrorBoundary FallbackComponent={Fallback}>
<RiskyComponent />
</ErrorBoundary>useId
// generate unique IDs for accessibility — SSR safe
function FormField({ label }: { label: string }) {
const id = useId()
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
)
}
// each instance gets a unique id like ':r1:', ':r2:'Last updated on