Suspense is a boundary that catches suspension. Place it where you want the loading UI to live. The component that actually fetches doesn't need to know about loading at all.
The mental model
Imagine your component tree as a hierarchy of <Suspense fallback={...}> boundaries. When any descendant calls use(somePendingPromise), React 'throws' (suspends), walks up the tree until it finds the nearest Suspense, and renders that boundary's fallback. When the promise resolves, React re-renders the suspended subtree and the fallback is replaced with real content.
You decide where the boundary sits. A boundary at the top of the page means the whole page shows a spinner; a boundary around a single panel means only that panel shows the spinner while the rest renders normally.
Granularity
Coarse: <Suspense fallback={<FullPageSpinner />}><App /></Suspense>. Easy, ugly UX.
Fine: a Suspense around the sidebar, another around the main panel, another around each list row. Beautiful streaming UX where parts of the page become interactive at their own pace.
Realistic: a couple of strategic boundaries. The page shell renders instantly, a Suspense around the data section streams in when ready. Don't try to over-granularize on day one.
Nested Suspense
Boundaries nest naturally. An inner boundary catches its own descendants' suspension first; if there's no inner boundary, the outer one takes over. Use nesting to scope the spinner to the smallest meaningful region.
The transition trick
When a user action (search, filter change) triggers new data, the page would normally flash to the Suspense fallback. useTransition (Track 7) tells React 'this state change is non-urgent; keep showing the old UI until the new one is ready.' The result is a smooth swap instead of a flash of spinner.