React 18 introduced two primitives for keeping the UI responsive during heavy work. useTransition returns [isPending, startTransition] and lets you mark a state update as low-priority: React will keep the current screen interactive while it processes the transition in the background, perfect for filtering a large list or switching tabs in a heavy view. useDeferredValue returns a deferred copy of a value that React updates after urgent work completes — handy for typing into a search box where the list re-filter is expensive. The difference is precise: useTransition wraps a setter (you trigger the update), useDeferredValue wraps a value (React decides priority). Pairing them with useMemo and Suspense gives a smooth experience even when caches revalidate in the background. flushSync is the escape hatch when you genuinely need to flush updates synchronously, for example before reading layout that depends on the new state — use it rarely, because it bypasses batching.
React 19 adds several new hooks. The use hook reads a Promise or a Context value and, unlike useContext, can be called inside conditions and loops. Promise reads integrate with Suspense: wrap the consuming tree in a boundary with a fallback to handle the suspended state. useOptimistic shows a predicted value while an async action is in flight and reconciles when the action resolves — perfect for chat messages, likes, and deletes. useActionState (which replaces useFormState) manages state tied to a form action and returns the latest result. useFormStatus, used inside any descendant of a <form>, exposes pending, data, method, and action. The <form action={asyncFn}> prop ties it all together, with Server Actions for the backend half. Errors thrown inside a Server Action bubble to the nearest error boundary or surface through useActionState; an ErrorBoundary class component (or the react-error-boundary library) wraps a subtree to handle render errors gracefully, since no built-in functional equivalent exists yet.
Server Components and Server Actions reshape where rendering and data fetching happen. A Server Component runs once on the server, ships no JavaScript, and can fetch data directly — but it cannot use hooks, because hooks require a client instance. The "use client" directive at the top of a file marks it as a Client Component; "use server" marks a function (or all functions in a file) as a Server Action callable from the client. A Client Component can render a Server Component only by passing it as children — composition, not direct import. Combine Server Components with Suspense boundaries: a child suspends on a Promise, and the boundary catches it, showing a fallback until resolved. For mutating data, revalidatePath('/items') and revalidateTag('items') invalidate Next.js caches from inside Server Actions, and redirect('/done') performs a navigation as part of the action. The cache() function memoizes calls per request, deduplicating data fetches across a render tree.
Outside the server/client split, dedicated data libraries are usually the best way to share async state across components. TanStack Query (useQuery, useMutation, useInfiniteQuery), SWR, and Apollo handle caching, deduplication, retries, background refetching, pagination, and optimistic updates — most of what you'd otherwise reinvent poorly in a useFetch hook. useInfiniteQuery returns { data, fetchNextPage, hasNextPage } for "load more" patterns. Pair mutations with onMutate for optimistic writes and onError to revert; invalidateQueries on success triggers background refetch. For client-only forms, React Hook Form is uncontrolled by default for performance; Formik and controlled patterns with useReducer are alternatives. The underlying pattern across all of these is the same: external state deserves an external store, and the store should integrate with Suspense and concurrent rendering rather than fighting them via useEffect.