Drawers, dialogs, tabs, and sheets pile up, and each one ends up as a useState in whatever file opened it. The result can’t be linked, the back button doesn’t close it, and nobody else knows it exists.

Before reaching for state, ask whether it’s a view. Think in REST terms:

Question If yes
Does it show one specific resource? route: /items/:id
Does it create or edit a resource? route: /items/new, /items/:id/edit
Is it a section someone would want to link to? route: /items?tab=activity
Is it a yes/no about an action already in progress? state: confirm dialogs stay local

Before

const [itemId, setItemId] = useState<string | null>(null);

<button onClick={() => setItemId("1")}>Item 1</button>
{itemId && <ItemDialog id={itemId} onClose={() => setItemId(null)} />}

After

<Route path="/items" element={<Items />}>
  <Route path=":id" element={<ItemDialog />} />
</Route>

// Items
<Link to="/items/1">Item 1</Link>
<Outlet />

The dialog reads its id from useParams(), and closing it is just a link back to /items. It’s shareable, survives a refresh, and shows up in the route table where anyone can find it.

For animating these in and out, see Route-modal + view transitions.