Application Refactor — Dynamic Route & Page Architecture
🧭 Motivation
Previously, routes in our frontend were defined statically in App.jsx.
Each page (e.g., /Product_All, /MarketPage_All, /AdSet_All) was manually registered with a <Route> and wrapped in a <Layout>.
While this worked, it caused several problems:
- Scalability issues: Adding or modifying pages required manual edits in multiple places. Once a page was added in the database, it wouldn't become visible until the frontend code was updated.
- Tight coupling: The frontend needed to “know” about backend page structures. It requires the front and backend devs being on the same page and up to date.
- Duplication: Pages with similar structure (like AdsLauncher, AdsetLauncher) repeated boilerplate logic. Every file was essentially the same, just going through different duplicated code.
- Limited flexibility: Couldn’t easily build new pages or update widgets without redeploying the frontend.
🌠 The Signs
The signs were already there. Because our backend returns data in a predictable and structured manner — a handful of widgets, each providing different data but following the same schema — the opportunity was clear.
Our TableTemplate component was already rendering all table cells dynamically through the generateDynamicColumns utility. Each cell’s JSX came from the Cell.jsx component, while table headers were handled by Header.jsx. Everything from the table level downward followed this dynamic, schema-driven approach.
Given that the backend consistently provides responses in this standardized format, it only made sense to extend this design philosophy across the entire application. By leaning into this predictability, we can build UI components that are completely data-driven, reusable, and far easier to maintain.
💡 Benefits
Adopting this consistent, backend-aware architecture brings several key benefits:
- TypeScript Integration – With a well-defined and predictable data structure, we can generate or infer TypeScript types automatically. This improves type safety, reduces runtime bugs, and makes refactoring far smoother. If in the future, we ever decide to migrate to TypeScript, this is going to make our lives so much easier.
- Reusability & Scalability – Components no longer need to be rebuilt for each data type or page. The same
TableTemplate,Cell, andHeaderlogic can render any dataset the backend returns — minimizing repetitive code. - Consistency in UI/UX – When all widgets, tables, and detail views follow the same schema-driven rendering model, the app feels coherent and uniform, making both development and user experience more predictable.
- Reduced Maintenance Overhead – Future backend changes (like adding a new widget or updating a field) won’t break the UI. The frontend dynamically adapts as long as the data structure stays consistent.
- Better Extensibility – This architecture makes it easy to introduce new views, widgets, or layouts without touching the core rendering logic. The system becomes flexible, modular, and easier to scale as the app grows.
⚙️ Things to Consider
While this dynamic, schema-driven approach unlocks a ton of flexibility, it also introduces some trade-offs and implementation caveats that need to be kept in mind.
- Loss of Granular Control – Since much of the rendering logic is now abstracted and data-driven, the frontend loses a degree of fine-tuned control over how certain widgets or elements behave. Some parts of the UI still rely on hardcoded logic until the backend fully assumes responsibility for defining structure and behavior.
- Layout Uncertainty – Previously, when each frontend page was tightly coupled to specific widgets, we knew exactly how many widgets would appear and in what order. This made it easy to manually craft precise layouts.
Now, because the frontend no longer has static knowledge of widget placement or count, layout rendering becomes more generic. All widgets may appear in a vertical stack (one after another), which can lead to design inconsistencies unless layout metadata is included in the backend response. - Order Dependency – The visual order of widgets depends directly on the order of the
widgetsarray returned by the backend. If the backend sends the array in a different sequence, the frontend layout changes accordingly. This can be both a feature (for flexible control) and a potential source of confusion (if ordering is inconsistent). - Need for Layout Metadata – To achieve consistent and intentional layouts in a fully dynamic system, the backend should eventually return additional layout data — such as grid positioning, column spans, or display priorities — so the frontend can render the structure accurately without hardcoded assumptions.
- Progressive Transition Phase – Until the backend completely owns the structure, certain pages or components will exist in a hybrid state — partially static, partially dynamic. This is a natural phase of the migration and should be handled carefully to avoid regressions or broken UI states.
- User Management — The user management has VERY different structure
To solve this, I, Muhammad Ali Khalid, began refactoring the app to support backend-driven dynamic routing — where the backend defines what pages exist, what data they show, and what widgets they contain — and the frontend automatically builds the UI accordingly.
🧩 What’s Been Done
1. Dynamic Routing Layer
🔹 Before
Routes were hardcoded in App.jsx:
<Route path="/Product_All" element={<Layout><ProductsPage /></Layout>} />🔹 After
We introduced a useDynamicRoutes() hook that loads route definitions (currently from a local JSON file, later from the backend):
{dynamicRoutes.map(route => {
const element =
route.layout === 'none' ? (
<PageContainer key={route.id} routeConfig={route} />
) : (
<Layout>
<PageContainer key={route.id} routeConfig={route} />
</Layout>
);
return <Route key={route.id} path={route.path} element={element} />;
})}Please go to this page for a detailed explanation about the JSON structure.
Each route object (e.g. from dynamicRoutes.json) describes:
{
"id": "products",
"path": "/Product_All",
"pageKey": "Product_All",
"apiPageName": "Product_All",
"paginationType": "server",
"public": false
}These route definitions are currently static but will eventually come from the backend, allowing the API to define and control all UI pages dynamically.
2. Page Container System
Each dynamic route renders a <PageContainer /> component that serves as a universal renderer for any backend-defined page.
PageContainer uses the PageDataProvider context to:
- Fetch data for the page using
useGetPageDataQuery - Pass this data to the
Widgetrenderer - Manage loading and error states
<PageDataProvider routeConfig={routeConfig}>
<PageContent />
</PageDataProvider>3. Context-Driven Page Data Layer
The PageDataContext (PageDataProvider + usePageData) abstracts all page-level data logic.
Each page fetches its content using apiPageName (e.g. "Product_All") and optional URL parameters (glid, page_name, etc.).
It automatically supports both client-side and server-side pagination depending on the route configuration.
Key features:
- Builds query parameters dynamically
- Integrates with RTK Query (
useGetPageDataQuery) - Exposes
serverTableOpsfor paginated widgets - Provides global loading/error states
This enables any widget to access shared page-level data or trigger a refresh via refetchPageData.
4. Widget Composition System
Each page is a list of widgets provided by the backend.
PageContainer renders widgets using the Widget component:
{widgets.map(widget => (
<Widget key={widget.id} widget={widget} apiPageName={apiPageName} />
))}The Widget component:
- Looks up the widget type in a centralized
widgetRegistry - Dynamically renders the corresponding React component
- Displays a warning if the widget type is unknown
Note: The widgetRegistry can also be returned by the backend, but even this removes a lot of coupling. This can be planned for the future.
export const widgetRegistry = {
page_header: PageHeaderWidget,
dgv: TableWidget,
paginated_dgv: TableWidget,
property_grid: PropertyGridWidget,
carousel: CarouselWidget,
};This makes the UI plug-and-play — adding a new widget is as simple as:
- Creating a new component
- Registering it in
widgetRegistry.jsx - Returning it from the backend
5. TableWidget Refactor — Modular Configuration Builders
The TableWidget is one of the most complex and central widgets.
It was refactored to delegate logic into a set of config generator utilities, improving readability and maintainability.
⚙️ Config Generators Overview
Utility | Purpose |
|---|---|
| Builds cell navigation rules — determines where a click leads (e.g., opening a detail page). There can be columns that lead to a store on shopify or to Facebook. This also handles internal nagivation withing the application. |
| Defines editable table cells and connects them to backend POST APIs. This is one of the most crucial configs as it is required by the Table to connect the POST apis. The POST apis are generated for every cell based on its type. |
| Generates configurations for row-level actions (launch, duplicate, delete, etc.). |
| Builds filter input configurations from column metadata. |
| Provides pagination info extraction, preference handling, and RTK table setup. |
These functions take widget.Column_header (provided by the backend) and produce React Table-ready configurations — meaning the backend effectively controls table behavior.
Example use in TableWidget:
const navigationConfig = useMemo(
() => buildNavigationConfig(widget?.Column_header, apiPageName),
[widget?.Column_header, apiPageName]
);Together, they turn backend definitions into fully interactive, functional tables without hardcoding columns or behaviors in the frontend.
🧱 Current Architecture Overview
App.jsx
├── useDynamicRoutes() → dynamicRoutes.json / backend
│ └── <Route path=... element={<PageContainer routeConfig={...} />}>
└── PageContainer
└── PageDataProvider (fetches API data)
└── PageContent
└── Widget (from widgetRegistry)
└── TableWidget / PageHeaderWidget / etc.
🧠 What’s Remaining
1. Backend Integration for Routes
Currently, dynamic routes are loaded from dynamicRoutes.json.
Next step: load them from an API endpoint, e.g. /api/routes, allowing admins to manage routes and pages from the backend CMS.
3. Improved Fallback & Error Handling
Enhance <ErrorBoundary> and widget-level fallback UIs for partial load failures.
✅ Summary
Area | Status | Description |
|---|---|---|
Dynamic routing | ✅ Done (static JSON) | Backend-driven setup ready, currently fed from local file |
Page container | ✅ Done | Universal data + layout manager |
Widget system | ✅ Done | Registry-based rendering for backend widgets |
TableWidget refactor | ✅ Done | Modularized with config generators |
Route API integration | 🔄 In progress | To be implemented from backend |
💡 Vision
This refactor moves the app from a hardcoded, static frontend toward a metadata-driven, backend-configurable platform.
Eventually, the backend will define:
- What routes exist
- What widgets each page contains
- What actions each widget supports
…and the frontend will automatically render the full experience — no frontend redeploys required.
Some more things that need consideration:
Hardcoded widgets on the Frontend:
The filters widget on the Product_All page:

filter-widget-on-products-page-business-lab
If this is returned by the backend when we receive the Products data, then it could simply be made into a widget and included on the Product_All page.
Add connection button on the All_Connexions page:

add-connection-button-on-All_Connexions-page
This can be hardcoded on the frontend, but it would again, be better to have a way for the backend to return this as the first widget. The +Add Connection button can then be made into a widget and then included on the page, controlled through the backend data. The actions it's going to perform will still be defined on the frontend of course.
The New Ad Account button on the campaign/ad-accounts page:

new-ad-account-button-on-ad-accounts-page
This can similarly be returned by the backend and the frontend would then handle it as a widget and define it's actions.
New Ad button on Ads Launcher page:

new-ad-button-on-ads-launcher-page
Same as above.
+ New Ad Set button on Adset Launcher page:

new-adset-button-on-adset-launcher-page
These things need to be handled for the application to be fully dynamic and with zero-hardcoding. If we are looking to make the frontend completely data-driven.