Is it a view?
A quick REST test for what gets a URL. If it shows or edits a resource, it's a route, not useState.
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.