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 sonnerFrontend only
The package covers the UI module and frontend logic only. Backend control-table APIs are unchanged.
What the package includes
| Layer | Exports |
|---|---|
| Entry | ReactApiTable, ApiTablesController, ApiTablesComponent |
| Host setup | ApiTablesHostProvider, useApiTablesConfig, setExternalStoreGetter |
| Hooks | useTableStructure, useTableFetcher, useUtilsProvider |
| State | Per-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 import | npm config field |
|---|---|
@/lib/axios | axiosTable |
@/store/store | getExternalStore + setExternalStoreGetter() |
@/components/ui/* | components map |
@/i18n/navigation Link | Link |
ApiTablesModals custom-control imports | customControls registry |
Bulk modals by action_key | bulkActionModals |
| Cart / loader side effects | onBulkActionSuccess, 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
| Field | Required | Purpose |
|---|---|---|
axiosTable | Yes | Structure GET, query POST, row/bulk POST |
components | Yes | Design-system primitives |
getExternalStore | Recommended | getExternalState() for global bridges |
useOutScopeRefresher | Recommended | Remount key from global Redux (state.app.outScopeTableRefresher) |
useLocale | Recommended | ln request header on structure GET |
usePathname | Recommended | Re-query when route changes |
useTranslations | Recommended | Filter operator labels and UI strings |
useSecureLS | Optional | Persist page size + column visibility |
storage | Optional | Fallback { get, set } when useSecureLS absent |
Link | If redirect actions | i18n-aware Link component |
cn | Optional | Tailwind class merge (defaults to clsx + tailwind-merge) |
customControls | If OpenModalForm | URL-matched modal forms |
bulkActionModals | If custom bulk UI | Matched by action_key |
rowActionModals | Optional | Extra row-response modals |
customElements | Optional | String slots like withdrawal_requests |
axiosAuth | Optional | Session delete on 401 |
onUnauthorized | Optional | Custom 401 handler |
onBulkActionSuccess | Optional | Cart count, route-specific reload side effects |
setMainLoader | Optional | Global overlay during bulk POST |
toast | Optional | { success, error } handlers |
downloadBlob | Optional | Excel blob export actions |
downloadPDF | Optional | PDF download actions |
generatePDF | Optional | Invoice PDF generation actions |
Components map
Every key below must exist in config.components if your tables use that feature. Names are case-sensitive.
| Key | Used for |
|---|---|
Button | Toolbar, filters, row actions, modals |
Badge | Sort badge, applied filter chips, boolean cells |
Card | Structure loading wrapper |
Skeleton | Inline loaders |
Popup | All modal shells |
FullPageTableLoader | Full-page skeleton while structure loads |
Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue | Filters, page size |
Input, InputWrapper | Text/number filters |
Label | Filter field labels |
Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger | Filter side panel |
Tooltip, TooltipContent, TooltipTrigger | Filter help text |
Popover, PopoverContent, PopoverTrigger | Row action overflow, column picker |
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger | Bulk actions menu |
Checkbox | Row selection, column visibility |
Switch | Boolean filters, toggle row actions |
Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow | View-row-data modal |
MultiSelect, MultiSelectContent, MultiSelectGroup, MultiSelectItem, MultiSelectTrigger, MultiSelectValue | Multi-select filters |
DatePicker | Date / datetime filters |
TagsInput | Tag-style filters |
LoadingButton | Row actions with POST loading state |
CustomPagination | Bottom pagination bar |
AppCurrency | Currency 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.payloadoraction_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:
| Type | How |
|---|---|
| React node | customElement={<MySlot />} on ReactApiTable — renders above the table |
| String key | customElement="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
/>| Prop | Purpose |
|---|---|
table | Structure JSON from useTableStructure |
tableStructureLoading | Shows FullPageTableLoader while true |
params | Extra POST payload for query-table |
customElement | React node or string key above table |
controller | Custom ApiTablesController subtree |
children | Rendered 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, BulkActionModalMatcherTroubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
useApiTablesConfig must be used within ApiTablesHostProvider | Page outside provider | Wrap layout or page |
components.Popup is required | Missing component key | Add to config.components |
| Structure loads, no rows | tableName missing or wrong axios base | Check network tab + structure response |
| Custom control modal blank | No URL match | Add customControls entry |
| Redirect action broken | No Link | Provide config.Link |
| Filters show raw keys | No i18n | Provide useTranslations |
| 401 does nothing | No auth handler | Set axiosAuth or onUnauthorized |
| Table state leaks between tables | Shared provider key | Each ReactApiTable creates its own ApiTablesProvider store |
Related docs
- Getting started — backend prerequisites and structure fields
- Providers — Redux and
useUtilsProvider - Table structure — JSON contract
- Package map — file-level map of the module
- npm package on npmjs.com — install and README