Menu

Login
Login

Frontend Route Registration & Page Architecture

By Unknown 11/3/2025

Frontend Route Registration & Page Architecture

Route Configuration Structure

This is the structure we use to register routes in our App.jsx file:

json
[
  {
    "id": "products",
    "path": "/Product_All",
    "pageKey": "Product_All",
    "apiPageName": "Product_All",
    "public": false
  },
  {
    "id": "connexions",
    "path": "/All_Connexions",
    "pageKey": "All_Connexions",
    "apiPageName": "Connexions_All",
    "public": false
  },
  {
    "id": "ad-accounts",
    "path": "/campaign/ad-accounts",
    "pageKey": "AdAccount_All",
    "apiPageName": "AdAccount_All",
    "public": false
  },
  {
    "id": "ads-launcher",
    "path": "/campaign/ads-launcher",
    "pageKey": "AdProject_All",
    "apiPageName": "AdProject_All",
    "public": false
  },
  {
    "id": "adsets",
    "path": "/AdSet_All",
    "pageKey": "AdSet_All",
    "apiPageName": "AdSet_All",
    "public": false
  },
  {
    "id": "market-ads",
    "path": "/MarketAds_All",
    "pageKey": "MarketAds_All",
    "apiPageName": "MarketAds_All",
    "paginationType": "server",
    "public": false
  },
  {
    "id": "market-pages",
    "path": "/MarketPage_All",
    "pageKey": "MarketPage_All",
    "apiPageName": "MarketPage_All",
    "paginationType": "server",
    "public": false
  },
  {
    "id": "market-products",
    "path": "/MarketProduct_All",
    "pageKey": "MarketProduct_All",
    "apiPageName": "MarketProduct_All",
    "paginationType": "server",
    "public": false
  },
  {
    "id": "product-details",
    "path": "/Product_Details",
    "pageKey": "Product_Details",
    "apiPageName": "Product_All",
    "public": false,
    "queryParams": {
      "glid": ":glid"
    }
  },
  {
    "id": "market-product-details",
    "path": "/MarketProduct_Details",
    "pageKey": "MarketProduct_Details",
    "apiPageName": "MarketProduct_Details",
    "public": false,
    "paginationType": "server",
    "queryParams": {
      "glid": ":id"
    }
  }
]

Design Considerations

This structure allows us to add all possible configuration information like:

  • Route guards
  • Layout information
  • Query parameters
  • Any other metadata we might need
  • paginationType helps us determine whether the page is going to server-paginated. Helps us with the initial api call, whether to include page_size, page_number etc in api url query params

Note: The handling of pages with query parameters (like glid or page_name) is still being refined.

Implementation

App.jsx

javascript
<ErrorBoundary
  FallbackComponent={() => (
    <ErrorBoundaryFallback resetErrorBoundary={resetErrorBoundary} />
  )}
>
  <Routes>
    {/* Dynamic routes from backend */}
    {dynamicRoutes.map(route => {
      const element =
        route.layout === 'none' ? (
          // unique key is required here so the component remounts on route change
          <PageContainer key={route.id} routeConfig={route} />
        ) : (
          <Layout>
            <PageContainer key={route.id} routeConfig={route} />
          </Layout>
        );

      return (
        <Route
          key={route.id || route.path}
          path={route.path}
          element={element}
        />
      );
    })}
  </Routes>
</ErrorBoundary>

The Layout component is a simple wrapper with a Sidebar that's populated from the backend.

PageContainer.jsx

This file contains two components: PageContent and PageContainer. PageContainer is the default export.

javascript
import { PageDataProvider, usePageData } from '../../context/PageDataContext';
import { Widget } from '../../widgets/Widget';

const PageContent = () => {
  const { data: widgets, apiPageName } = usePageData();

  // the data is shaped like this:
  // [{...widget1}, {...widget2}, {...widget3}]

  return (
    <div className='w-full space-y-6'>
      {widgets.map((widget, idx) => (
        <Widget
          key={`${widget?.dgv_id || widget?.widget_name || idx}`}
          widget={widget}
          apiPageName={apiPageName}
        />
      ))}
    </div>
  );
};

export default function PageContainer({ routeConfig }) {
  return (
    <PageDataProvider routeConfig={routeConfig}>
      <PageContent />
    </PageDataProvider>
  );
}

PageDataContext.jsx

The provider fetches page data based on the apiPageName:

javascript
import { createContext, useContext, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useGetPageDataQuery } from '../services/page';
import LoadingState from '../components/states/LoadingState';
import ErrorState from '../components/states/ErrorState';

const PageDataContext = createContext(null);

/**
 * PageDataProvider
 * - Fetches page data based on routeConfig.apiPageName
 * - Provides page data, loading/error states, and params to children
 */
export const PageDataProvider = ({ routeConfig, children }) => {
  const [searchParams] = useSearchParams();
  const paramsObject = Object.fromEntries(searchParams.entries());
  const glid = paramsObject['glid'];

  const apiPageName = routeConfig?.apiPageName;

  const {
    data,
    isLoading,
    isError,
    error,
    isFetching,
    refetch: refetchPageData,
  } = useGetPageDataQuery({ page: apiPageName }, { skip: !apiPageName });

  const value = useMemo(
    () => ({
      apiPageName,
      data,
      glid,
      isLoading,
      isError,
      isFetching,
      error,
      refetchPageData,
    }),
    [
      apiPageName,
      data,
      glid,
      isLoading,
      isError,
      isFetching,
      error,
      refetchPageData,
    ]
  );

  if (isLoading) return <LoadingState />;
  if (isError) return <ErrorState error={error} />;
  if (!Array.isArray(data) || data.length === 0) {
    return (
      <div className='p-4'>
        <div className='text-gray-600'>
          No content available for {apiPageName}
        </div>
      </div>
    );
  }

  return (
    <PageDataContext.Provider value={value}>
      {children}
    </PageDataContext.Provider>
  );
};

/**
 * Hook to use page data anywhere in the tree
 */
export const usePageData = () => {
  const context = useContext(PageDataContext);
  if (!context) {
    throw new Error('usePageData must be used within a PageDataProvider');
  }
  return context;
};

Key Feature: The context makes all page data automatically available to ANY component down the tree that needs it.

Widget System

Widget.jsx

The main widget renderer that looks up widget types and renders appropriate components:

javascript
// widgets/Widget.jsx - Cleaner widget renderer
import { widgetRegistry } from './widgetRegistry';

/**
 * Widget Component
 * - Looks up widget type in registry
 * - Renders appropriate component
 * - Shows error state for unknown widgets
 */
export function Widget({ widget, apiPageName, index }) {
  const WidgetComponent = widgetRegistry[widget.type];

  if (!WidgetComponent) {
    console.warn(`Unknown widget type: ${widget.type}`, widget);

    return (
      <div className='p-4 border-2 border-dashed border-yellow-400 rounded-lg bg-yellow-50'>
        <div className='flex items-center gap-2 text-yellow-800'>
          <span className='text-lg'>⚠️</span>
          <div>
            <div className='font-semibold'>Unknown Widget Type</div>
            <div className='text-sm'>Type: {widget.type}</div>
            <div className='text-xs mt-1 text-gray-600'>
              Widget ID: {widget.dgv_id || widget.widget_name || index}
            </div>
          </div>
        </div>
      </div>
    );
  }

  return (
    <WidgetComponent widget={widget} apiPageName={apiPageName} index={index} />
  );
}

widgetRegistry.jsx

Maps backend widget types to React components:

javascript
// widgets/index.js - Centralized widget exports and registry

import TableWidget from './TableWidget';
import PageHeaderWidget from './PageHeaderWidget';
// ... other widget imports

/**
 * Widget Registry
 * Maps backend widget types to React components
 * 
 * Each widget receives: { widget, apiPageName, pageData }
 */
export const widgetRegistry = {
  'page_header': PageHeaderWidget,
  'dgv': TableWidget,
  'paginated_dgv': TableWidget,
  // Add more as needed
};

Note: We could fetch this registry from the backend, but keeping it on the frontend keeps things simple for now.

Widget Implementation

Each widget (like TableWidget) handles its own logic internally and passes data to its template component (like <TableTemplate />).

Please note that the User_All will most likely be excluded from this page system, because it is too different from every other page in the webstie.