React Expert
Overview
Advanced expertise in React — from core hooks and component patterns to state management, performance optimization, and React 18/19 concurrent features.
1. Core Hooks
// useState — local state
const [count, setCount] = useState(0)
const [user, setUser] = useState<User | null>(null)
// useEffect — side effects & cleanup
useEffect(() => {
const sub = subscribe(id, callback)
return () => sub.unsubscribe() // cleanup
}, [id]) // dependency array
// useRef — mutable ref, DOM access, skip re-render
const inputRef = useRef<HTMLInputElement>(null)
const timerRef = useRef<ReturnType<typeof setInterval>>(null)
// useMemo — memoize expensive computation
const sorted = useMemo(() => [...items].sort(compareFn), [items])
// useCallback — stable function reference
const handleClick = useCallback((id: string) => {
setSelected(id)
}, []) // empty = stable across renders
// useReducer — complex state logic
const [state, dispatch] = useReducer(reducer, initialState)
// useContext — consume context
const theme = useContext(ThemeContext)
// useId — stable unique IDs (React 18+)
const id = useId()
2. Custom Hooks
// Fetching with loading/error state
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d) })
.catch(e => { if (!cancelled) setError(e) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Debounced value
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(t)
}, [value, delay])
return debounced
}
// Local storage sync
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
try { return JSON.parse(localStorage.getItem(key) ?? '') }
catch { return initial }
})
const set = useCallback((v: T) => {
setValue(v)
localStorage.setItem(key, JSON.stringify(v))
}, [key])
return [value, set] as const
}
3. Component Patterns
Compound Components
const Tabs = ({ children, defaultTab }: TabsProps) => {
const [active, setActive] = useState(defaultTab)
return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>
}
Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panel = TabPanel
// Usage
<Tabs defaultTab="home">
<Tabs.List>
<Tabs.Tab id="home">Home</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="home">...</Tabs.Panel>
</Tabs>
Render Props
function MouseTracker({ render }: { render: (pos: Position) => ReactNode }) {
const [pos, setPos] = useState({ x: 0, y: 0 })
return <div => setPos({ x: e.clientX, y: e.clientY })}>{render(pos)}</div>
}
Controlled vs Uncontrolled
// Controlled — React owns the value
<input value={value} => setValue(e.target.value)} />
// Uncontrolled — DOM owns the value, use ref to read
const ref = useRef<HTMLInputElement>(null)
<input defaultValue="initial" ref={ref} />
Forwarding Refs
const Input = forwardRef<HTMLInputElement, InputProps>(({ label, ...props }, ref) => (
<label>
{label}
<input ref={ref} {...props} />
</label>
))
4. State Management
Context API (light global state)
interface AuthContextType { user: User | null; logout: () => void }
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const logout = useCallback(() => setUser(null), [])
return <AuthContext.Provider value={{ user, logout }}>{children}</AuthContext.Provider>
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used inside AuthProvider')
return ctx
}
Zustand (recommended for most apps)
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
total: () => number
}
const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: item => set(s => ({ items: [...s.items, item] })),
removeItem: id => set(s => ({ items: s.items.filter(i => i.id !== id) })),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}),
{ name: 'cart' }
)
)
Redux Toolkit
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit'
const fetchUser = createAsyncThunk('user/fetch', async (id: string) => {
const res = await fetch(`/api/users/${id}`)
return res.json() as Promise<User>
})
const userSlice = createSlice({
name: 'user',
initialState: { data: null as User | null, status: 'idle' },
reducers: {
clearUser: state => { state.data = null },
},
extraReducers: builder => {
builder
.addCase(fetchUser.fulfilled, (state, action) => { state.data = action.payload; state.status = 'success' })
.addCase(fetchUser.pending, state => { state.status = 'loading' })
}
})
5. Data Fetching — TanStack Query (React Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Query
const { data, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000, // 5 min
enabled: !!id,
})
// Mutation with optimistic update
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async newUser => {
await queryClient.cancelQueries({ queryKey: ['user', newUser.id] })
const prev = queryClient.getQueryData(['user', newUser.id])
queryClient.setQueryData(['user', newUser.id], newUser)
return { prev }
},
onError: (_, __, ctx) => {
queryClient.setQueryData(['user', _.id], ctx?.prev)
},
onSettled: (_, __, vars) => {
queryClient.invalidateQueries({ queryKey: ['user', vars.id] })
},
})
6. React 18 / 19 Features
// Concurrent rendering — startTransition (non-urgent updates)
import { startTransition, useTransition, useDeferredValue } from 'react'
const [isPending, startTransition] = useTransition()
startTransition(() => setSearchQuery(input)) // won't block UI
// useDeferredValue — defer expensive re-renders
const deferred = useDeferredValue(searchQuery)
// Suspense for data fetching (with React Query / SWR)
<Suspense fallback={<Spinner />}>
<UserProfile id={id} />
</Suspense>
// React 19 — use() hook
import { use } from 'react'
function UserName({ promise }: { promise: Promise<User> }) {
const user = use(promise) // suspends until resolved
return <span>{user.name}</span>
}
// React 19 — Actions (form handling)
function Form() {
const [state, formAction, isPending] = useActionState(submitAction, null)
return <form action={formAction}>...</form>
}
// React 19 — useOptimistic
const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (state, n) => state + n)
7. Performance Optimization
// React.memo — skip re-render if props unchanged
const ExpensiveList = memo(({ items }: { items: Item[] }) => (
<ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
))
// Avoid inline object/array props (new reference every render)
// Bad:
<List style={{ margin: 0 }} items={[1, 2, 3]} />
// Good:
const style = useMemo(() => ({ margin: 0 }), [])
const items = useMemo(() => [1, 2, 3], [])
// Virtualize long lists — @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual'
// Code splitting — lazy + Suspense
const Dashboard = lazy(() => import('./Dashboard'))
// Profiler — measure render costs
<Profiler id="App" phase, actualDuration) => console.log(id, phase, actualDuration)}>
<App />
</Profiler>
8. Error Boundaries
// Class component (still required for error boundaries)
class ErrorBoundary extends Component<{ fallback: ReactNode; children: ReactNode }, { error: Error | null }> {
state = { error: null }
static getDerivedStateFromError(error: Error) { return { error } }
componentDidCatch(error: Error, info: ErrorInfo) { logError(error, info) }
render() {
return this.state.error ? this.props.fallback : this.props.children
}
}
// react-error-boundary library (recommended)
import { ErrorBoundary } from 'react-error-boundary'
<ErrorBoundary fallbackRender={({ error, resetErrorBoundary }) => (
<div>Error: {error.message} <button
)}>
<App />
</ErrorBoundary>
9. React Router v6/v7
import { createBrowserRouter, RouterProvider, Outlet, useParams, useNavigate, useLoaderData } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{
path: 'users/:id',
element: <UserDetail />,
loader: async ({ params }) => fetchUser(params.id!),
},
{ path: '*', element: <NotFound /> },
],
},
])
// In component
const { id } = useParams()
const user = useLoaderData() as User
const navigate = useNavigate()
navigate('/users/123', { replace: true })
10. TypeScript Patterns
// Component prop types
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary'
loading?: boolean
}
// Generic component
function List<T extends { id: string }>({ items, renderItem }: {
items: T[]
renderItem: (item: T) => ReactNode
}) {
return <ul>{items.map(item => <li key={item.id}>{renderItem(item)}</li>)}</ul>
}
// Event handlers
const onChange: React.ChangeEventHandler<HTMLInputElement> = e => setValue(e.target.value)
const onSubmit: React.FormEventHandler<HTMLFormElement> = e => { e.preventDefault(); ... }
// Discriminated unions for component state
type State =
| { status: 'loading' }
| { status: 'error'; error: Error }
| { status: 'success'; data: User }
Core Competency Summary
- Build composable, type-safe React components with TypeScript
- Master hooks:
useState,useEffect,useMemo,useCallback,useRef, custom hooks - Manage state with Zustand, Context API, or Redux Toolkit
- Fetch and cache data with TanStack Query
- Leverage React 18/19 concurrent features:
Suspense,useTransition,useOptimistic - Optimize performance:
memo,useMemo,useCallback, code splitting, virtualization - Handle routing with React Router v6/v7