npm package

Install and integrate storage-apitables — config, components, custom controls, and advanced usage

ApiTables is published on npm as storage-apitables by Mohamed Hasan (@mhmdhasan).

Install

pnpm add storage-apitables
pnpm add react react-dom react-redux @reduxjs/toolkit axios react-data-table-component sonner

Frontend only

The package covers the UI module and frontend logic only. Backend control-table APIs are unchanged.

What the package includes

LayerExports
EntryReactApiTable, ApiTablesController, ApiTablesComponent
Host setupApiTablesHostProvider, useApiTablesConfig, setExternalStoreGetter
HooksuseTableStructure, useTableFetcher, useUtilsProvider
StatePer-table Redux slices + createTableStore

Not in the package: backend endpoints, feature-specific custom-controls forms, your axios instances, and global app Redux — you provide those through config.

How it differs from an in-app copy

An embedded ./ApiTables/ folder imports app code directly (@/components/ui/*, @/store/store, hardcoded modals). The npm package replaces those with ApiTablesHostProvider:

Embedded importnpm config field
@/lib/axiosaxiosTable
@/store/storegetExternalStore + setExternalStoreGetter()
@/components/ui/*components map
@/i18n/navigation LinkLink
ApiTablesModals custom-control importscustomControls registry
Bulk modals by action_keybulkActionModals
Cart / loader side effectsonBulkActionSuccess, setMainLoader

Architecture

Step 1 — Bootstrap global store bridge

If your app uses getExternalState() bridges (cart, loader, remount key), register the store once at startup:

import { setExternalStoreGetter } from 'storage-apitables';
import { store } from '@/store/store';

setExternalStoreGetter(() => store);

Step 2 — Create a shared host provider

Do not repeat the full config on every table page. Create one module:

'use client';

import { useSelector } from 'react-redux';
import { ApiTablesHostProvider, type ApiTablesConfig } from 'storage-apitables';
import { axiosTable } from '@/lib/axios';
import { store } from '@/store/store';
import { Link, usePathname } from '@/i18n/navigation';
import { useLocale, useTranslations } from 'next-intl';
import useSecureLS from '@/hooks/useSecureLS';
import Popup from '@/components/Popup';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
// …remaining primitives — see "Components map" below

import { EditProduct } from '@/features/tables/custom-controls/EditProduct';
import { EditCategory } from '@/features/tables/custom-controls/EditCategory';
import { AddToCreditBulkModal } from '@/features/tables/custom-controls/AddToCreditBulkModal';

export function buildApiTablesConfig(): ApiTablesConfig {
  return {
    axiosTable,
    getExternalStore: () => store,
    useOutScopeRefresher: () =>
      useSelector((s: { app: { outScopeTableRefresher: unknown } }) => s.app.outScopeTableRefresher),
    useLocale,
    useTranslations,
    usePathname,
    useSecureLS,
    Link,
    components: {
      Button,
      Card,
      Badge,
      Popup,
      Select,
      SelectContent,
      SelectItem,
      SelectTrigger,
      SelectValue,
      // …complete map below
    },
    customControls: [
      { match: '/cl/products', component: EditProduct },
      { match: '/cl/categories', component: EditCategory },
    ],
    bulkActionModals: [
      { actionKey: 'create_withdrawal_bulk', component: AddToCreditBulkModal },
    ],
    onBulkActionSuccess: (action, response) => {
      // Route-specific side effects, e.g. cart count after bulk reload
    },
    setMainLoader: ({ status, msg, icon }) => {
      store.dispatch(_setMainLoader({ status, msg, icon }));
    },
  };
}

export function ApiTablesHost({ children }: { children: React.ReactNode }) {
  return (
    <ApiTablesHostProvider config={buildApiTablesConfig()}>
      {children}
    </ApiTablesHostProvider>
  );
}

Wrap your authenticated layout or each table route:

<ApiTablesHost>{children}</ApiTablesHost>

Step 3 — Wire a table page

'use client';

import { useEffect } from 'react';
import { ReactApiTable, useTableStructure } from 'storage-apitables';

export default function OrdersTablePage() {
  const { getTableStructure, tableStructure, tableStructureLoading } = useTableStructure();

  useEffect(() => {
    getTableStructure({
      table: '/api-table/control-tables/load-table/orders',
    });
  }, [getTableStructure]);

  return (
    <ReactApiTable
      table={tableStructure}
      tableStructureLoading={tableStructureLoading}
    />
  );
}

Prerequisites

  • Backend load-table endpoint for your table name
  • Backend query-table endpoint for row data
  • Page is 'use client' (Next.js App Router)
  • Page is inside ApiTablesHostProvider

Field shapes: Table structure.

ApiTablesConfig — full reference

FieldRequiredPurpose
axiosTableYesStructure GET, query POST, row/bulk POST
componentsYesDesign-system primitives
getExternalStoreRecommendedgetExternalState() for global bridges
useOutScopeRefresherRecommendedRemount key from global Redux (state.app.outScopeTableRefresher)
useLocaleRecommendedln request header on structure GET
usePathnameRecommendedRe-query when route changes
useTranslationsRecommendedFilter operator labels and UI strings
useSecureLSOptionalPersist page size + column visibility
storageOptionalFallback { get, set } when useSecureLS absent
LinkIf redirect actionsi18n-aware Link component
cnOptionalTailwind class merge (defaults to clsx + tailwind-merge)
customControlsIf OpenModalFormURL-matched modal forms
bulkActionModalsIf custom bulk UIMatched by action_key
rowActionModalsOptionalExtra row-response modals
customElementsOptionalString slots like withdrawal_requests
axiosAuthOptionalSession delete on 401
onUnauthorizedOptionalCustom 401 handler
onBulkActionSuccessOptionalCart count, route-specific reload side effects
setMainLoaderOptionalGlobal overlay during bulk POST
toastOptional{ success, error } handlers
downloadBlobOptionalExcel blob export actions
downloadPDFOptionalPDF download actions
generatePDFOptionalInvoice PDF generation actions

Components map

Every key below must exist in config.components if your tables use that feature. Names are case-sensitive.

KeyUsed for
ButtonToolbar, filters, row actions, modals
BadgeSort badge, applied filter chips, boolean cells
CardStructure loading wrapper
SkeletonInline loaders
PopupAll modal shells
FullPageTableLoaderFull-page skeleton while structure loads
Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValueFilters, page size
Input, InputWrapperText/number filters
LabelFilter field labels
Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTriggerFilter side panel
Tooltip, TooltipContent, TooltipTriggerFilter help text
Popover, PopoverContent, PopoverTriggerRow action overflow, column picker
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTriggerBulk actions menu
CheckboxRow selection, column visibility
SwitchBoolean filters, toggle row actions
Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRowView-row-data modal
MultiSelect, MultiSelectContent, MultiSelectGroup, MultiSelectItem, MultiSelectTrigger, MultiSelectValueMulti-select filters
DatePickerDate / datetime filters
TagsInputTag-style filters
LoadingButtonRow actions with POST loading state
CustomPaginationBottom pagination bar
AppCurrencyCurrency custom element slot

A missing key throws at runtime when that UI renders.

Custom controls

When a row action returns onSuccess: "OpenModalForm", the package prefetches form data, stores it in rowActions.customControlAction, and renders the first matching control:

customControls: [
  { match: '/cl/products', component: EditProduct },
  { match: /\/announces\/slides/, component: EditAnnounceSlide },
],

Match runs against customControlAction.url.api and .url.web. Order matters — first match wins.

Control component contract

type Props = {
  action: Record<string, unknown>;
  closeModal: () => void;
  rowRefetch?: (row: Record<string, unknown>) => void;
};
  • Read defaults from action.payload or action_payload
  • Use useUtilsProvider() for submit handlers
  • Call closeModal() when done

See Custom controls overview and Wired controls for behavior details.

Bulk action modals

Standard bulk actions use the built-in confirmation modal. Custom UI registers by action_key:

bulkActionModals: [
  { actionKey: 'create_withdrawal_bulk', component: AddToCreditBulkModal },
],

When selectedBulkAction.action_key matches, the package skips the default confirmation and renders your component inside Popup.

Row action response modals

For special DisplayOnModal variants (e.g. announcement preview), use rowActionModals:

rowActionModals: [
  {
    match: '/new_features_announcements/show_details',
    component: AnnouncementSingle,
  },
],

Custom elements

Two extension points:

TypeHow
React nodecustomElement={<MySlot />} on ReactApiTable — renders above the table
String keycustomElement="withdrawal_requests" + customElements={{ withdrawal_requests: CalculateWithdrawalAmount }}

String keys inject into the table body via Table custom elements.

Advanced ReactApiTable props

<ReactApiTable
  table={tableStructure}
  tableStructureLoading={tableStructureLoading}
  params={{ status: 'active' }}              // merged into query-table POST body
  customElement={<ExportButton />}           // slot above toolbar
  controller={<MyApiTablesController />}   // replace default controller
/>
PropPurpose
tableStructure JSON from useTableStructure
tableStructureLoadingShows FullPageTableLoader while true
paramsExtra POST payload for query-table
customElementReact node or string key above table
controllerCustom ApiTablesController subtree
childrenRendered inside provider before the table

Data flow after mount

Structure fetch

useTableStructure GETs the load-table URL, sets local React state (not table Redux).

Controller init

ApiTablesController hydrates Redux slices from structure: columns, filters, bulk actions, row actions.

Query loop

useTableFetcher POSTs on changes to currentPage, pageSize, appliedFilters, tableSorting, tableRefresher, or pathname.

Actions

useUtilsProvider POSTs row/bulk actions. onSuccess values (reload, deleteRow, refetchRow, OpenModalForm, …) drive refresh rules.

Deep dive: Data flow.

Exported API summary

// Setup
ApiTablesHostProvider, useApiTablesConfig, useApiTablesComponent
setExternalStoreGetter, getExternalState, createTableStore

// Typical page
ReactApiTable, useTableStructure

// Lower-level
ApiTablesProvider, ApiTablesController, ApiTablesComponent
useTableFetcher, useUtilsProvider

// Types
ApiTablesConfig, ApiTablesComponents, CustomControlMatcher, BulkActionModalMatcher

Troubleshooting

SymptomLikely causeFix
useApiTablesConfig must be used within ApiTablesHostProviderPage outside providerWrap layout or page
components.Popup is requiredMissing component keyAdd to config.components
Structure loads, no rowstableName missing or wrong axios baseCheck network tab + structure response
Custom control modal blankNo URL matchAdd customControls entry
Redirect action brokenNo LinkProvide config.Link
Filters show raw keysNo i18nProvide useTranslations
401 does nothingNo auth handlerSet axiosAuth or onUnauthorized
Table state leaks between tablesShared provider keyEach ReactApiTable creates its own ApiTablesProvider store

On this page