Back to articles
ERPPublished: August 1, 2025Reading time: 7 min read

Why I Replaced Nested Modals with Routed Workspaces in an ERP

How complex child resources outgrew modal-only management and evolved into parent-aware routes, persistent tabs, and coordinated ERP workspaces.

UX ArchitectureRoutingState Management

The modal was reasonable at first. A parent record had a small child list, and opening that list without leaving the page was faster than designing a separate destination. Then the child gained its own CRUD. Editing one of its rows opened another modal. Related child management followed, and the “small contextual interaction” had quietly become a full page rendered inside an overlay.

The implementation still worked in the narrow sense that the controls opened and mutations ran. The interaction model no longer matched the resource. Parent context lived in component state, refresh could remove that context, browser history did not describe where the user was, and a table plus form plus another child workflow had very little room to work.

The resource outgrew its container

Nested modal flow

  • parent page
  • child management modal
  • edit modal
  • another nested resource
  • context held in local state

Routed resource flow

  • parent route
  • parent-aware child route
  • deeper child route when needed
  • refreshable context
  • space for full-page work

When a child becomes a workspace

I did not conclude that modals were generally wrong. They remained useful for confirmations, focused forms, uploads, and selection workflows. The boundary was about the resource, not the component style.

A child stopped feeling like modal content when it had independent CRUD, meaningful filtering and table state, its own permissions or workflow, several actions, or another child below it. At that point it had identity worth representing in the URL. It also needed enough layout and navigation space to behave like a workspace rather than a temporary interruption.

Moving parent context into the route

The first architectural change was to give complex child resources their own React Router destinations. The route carried the parent identifier, and the child page derived its scoped query from that parameter. It also loaded enough parent data to reconstruct a title and breadcrumb.

/parents/:parentId/children
/parents/:parentId/children/:childId/details

These are simplified public examples, not the internal paths. The important part is that the parent identity no longer depended on the parent component remaining mounted. A refresh could reload the same scope. A link could open that workspace directly. Browser back and forward had a meaningful resource boundary to navigate.

I first applied this direction to existing Workflow child-management screens. The underlying Workflow CRUD and business contracts were partly team-owned; my contribution was moving their frontend integration from modal-only management into parent-aware routes, connecting route parameters to their data, and retaining context through the application layout.

The hierarchy became reconstructable from navigation
Parent resource
Child routescope comes from the URL
Parent queryrestore the visible context
Breadcrumbshow where this resource belongs
Nested child routeadd depth without another management modal

Breadcrumbs were part of the state model

The breadcrumb was not added as decoration. Inside a deep ERP flow, the user needs to know which parent the current table belongs to. A generic heading such as “Steps” or “Parts” is not enough when several parents can expose the same type of child resource.

Each routed child page queried its selected parent and rendered that context in the page hierarchy. This made the page understandable after refresh or direct entry, not only after clicking the action that originally opened it. It also forced a useful implementation discipline: if the page could not rebuild its context from route state and data, it was still coupled to the old modal flow.

Routes solved identity, not multitasking

Independent routes fixed deep linking and context, but they exposed another ERP behavior. People often move between several resources before finishing any one of them. They may inspect one configuration, compare it with another, return to a filtered table, and then open a nested resource. Plain browser-style navigation made those movements serial even though the work was not.

I added a desktop workspace layer over React Router. Open tabs represented route contexts rather than arbitrary component instances. The URL remained the identity boundary: navigating to a path created or activated its tab, and opening an already known path selected the existing workspace instead of duplicating it.

Sidebar navigation used the same tab-aware path. Nested resources could supply parent-aware titles, so two child workspaces did not have to appear under the same generic label. Closing the active tab selected a nearby open workspace, and closing the final one returned to a safe home destination.

One coordinated navigation model
Current route
Active tabthe route currently being shown
Open workspacesdeduplicated by path
Sidebaropens or activates the same route identity
Nested titleadds parent context to the workspace
Table statecoordinated with workspace lifecycle
Browser historystill reflects route navigation

Temporary navigation versus retained work

Keeping every visited page open creates a different problem: the tab bar becomes a history dump. The workspace later distinguished temporary and retained tabs. Normal navigation could use one replaceable temporary tab. If a user wanted to keep that context, they could pin it. Opening important nested workspaces could retain them immediately.

Meaningful table interaction could also promote a temporary tab. Once a user had searched, filtered, sorted, selected rows, or changed pages, that destination was no longer just a preview. Preserving it reduced the chance that the next navigation silently replaced active work.

Reordering and clear-all controls followed, but they were secondary to the model: temporary tabs represented exploration; retained tabs represented work worth keeping visible.

The route, tab, and table could not each be truth

The technical risk was synchronization. React Router, the active tab, the set of open paths, Sidebar state, dynamic titles, and persisted table state all described part of the current workspace. If each updated independently, closing a tab could remove the wrong table state, route changes could reopen a tab that had just been closed, or the Sidebar could disagree with the visible content.

The implementation centralized tab state and synchronized it with route changes, but some lifecycle edges remained imperfect. Early close-time cleanup could target the current route instead of the path actually being closed. Browser-history handling was not uniform across every navigation path. Keyboard semantics for one close control also remained incomplete. Those are useful reminders that a tab bar is not only presentation; it is a second navigation surface that must obey the same state contract.

Persisted workspaces also needed a session boundary. Later login handling cleared the tab set before returning to the application. Broader logout, restart, and user-scoping behavior was not fully established in the history, so I would not claim a complete session model from persistence alone.

Desktop workspaces did not become mobile tabs

The persistent tab strip was a desktop workspace. The compact-screen shell already used destination-oriented bottom navigation. Reproducing a crowded desktop tab strip on a narrow screen would preserve the component but lose the reason it existed.

The responsive decision was to keep these interaction models distinct. Desktop could expose several retained route contexts at once. Mobile kept a smaller destination control and let each routed page occupy the available space. The underlying route hierarchy still worked on both; only the workspace chrome changed.

What still belonged in a modal

Moving management resources to routes did not remove modals from the ERP. A confirmation should not become a page. A focused create/edit form may still be clearer as a modal within its routed list. File upload and row selection can remain temporary interactions when they do not have their own navigation depth.

The boundary I still use is simple. If the interaction is focused and ends when the overlay closes, a modal is a good fit. If the resource has its own identity, table state, history, workflow, or children, it deserves a route. When users need several of those routed contexts alive at once, a workspace layer becomes useful—but only if the URL, active tab, and persisted state continue to describe the same place.

ERPOctober 25, 20269 min read

Building a Shared Data Table for an ERP: Who Owns the State?

How a client-side list component grew into separate client and server tables, why TanStack Table was tried and not adopted, and how table state moved from the URL to per-table persisted stores.

Frontend ArchitectureState Management