From 975ce4d7a82ac9fdb8b134cf0dd3c97c8ddf47c0 Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sun, 18 Jan 2026 09:18:45 +0530 Subject: [PATCH 1/5] feat: Add DocTypeTable component for Issue #75 - Compose useFrappeGetDocList hook with ListView component - All parameters transparent through DocTypeTable - Support loading, error, and empty states - Custom component overrides - Full TypeScript support - Comprehensive Storybook documentation --- .../src/components/docTypeTable/README.md | 304 ++++++++++++++++++ .../docTypeTable/docTypeTable.stories.tsx | 234 ++++++++++++++ .../components/docTypeTable/docTypeTable.tsx | 186 +++++++++++ .../src/components/docTypeTable/index.ts | 1 + .../src/components/hooks/index.ts | 1 + .../components/hooks/useFrappeGetDocList.ts | 128 ++++++++ .../frappe-ui-react/src/components/index.ts | 1 + 7 files changed, 855 insertions(+) create mode 100644 packages/frappe-ui-react/src/components/docTypeTable/README.md create mode 100644 packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.stories.tsx create mode 100644 packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx create mode 100644 packages/frappe-ui-react/src/components/docTypeTable/index.ts create mode 100644 packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts diff --git a/packages/frappe-ui-react/src/components/docTypeTable/README.md b/packages/frappe-ui-react/src/components/docTypeTable/README.md new file mode 100644 index 00000000..e1d06da8 --- /dev/null +++ b/packages/frappe-ui-react/src/components/docTypeTable/README.md @@ -0,0 +1,304 @@ +# DocTypeTable Component + +## Overview + +The `DocTypeTable` component is a general-purpose React component for displaying a table of Frappe documents. It combines the `useFrappeGetDocList` hook with the ListView component to provide a seamless integration with your Frappe backend. + +## Features + +- ✅ **Automatic Data Fetching**: Fetches data from Frappe using `useFrappeGetDocList` hook +- ✅ **Configurable Columns**: Define custom columns with labels, keys, and widths +- ✅ **Flexible Filtering**: Support for Frappe document filtering +- ✅ **Ordering**: Sort documents by any field +- ✅ **Pagination**: Built-in limit and offset support +- ✅ **Selection**: Optional row selection with banner +- ✅ **Loading States**: Show loading indicator while fetching +- ✅ **Error Handling**: Display errors with retry capability +- ✅ **Customizable UI**: Custom loading, empty, and error components +- ✅ **Callbacks**: Hooks for data load and error events + +## Installation + +The component is exported from the main package: + +```tsx +import { DocTypeTable } from '@rtcamp/frappe-ui-react'; +``` + +## Basic Usage + +```tsx +import { DocTypeTable } from '@rtcamp/frappe-ui-react'; + +function MyApp() { + const columns = [ + { + label: 'Name', + key: 'name', + width: 3, + }, + { + label: 'Email', + key: 'email', + width: '200px', + }, + ]; + + return ( + + ); +} +``` + +## Props + +### DocTypeTableProps + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `doctype` | `string` | **required** | The Frappe DocType name (e.g., 'User', 'Employee') | +| `columns` | `any[]` | **required** | Column configuration array | +| `rowKey` | `string` | `'name'` | The unique key field for each row | +| `params` | `FrappeGetDocListParams` | `{}` | Parameters for useFrappeGetDocList (fields, filters, etc.) | +| `options` | `ListOptionsProps` | - | ListView options for customization | +| `auto` | `boolean` | `true` | Auto-fetch data on component mount | +| `loadingComponent` | `ReactNode` | - | Custom loading indicator | +| `emptyComponent` | `ReactNode` | - | Custom empty state component | +| `errorComponent` | `ReactNode \| Function` | - | Custom error component | +| `onDataLoad` | `(data: any[]) => void` | - | Callback when data is loaded | +| `onError` | `(error: any) => void` | - | Callback when error occurs | + +### FrappeGetDocListParams + +Parameters passed to the API call: + +| Param | Type | Description | +|-------|------|-------------| +| `doctype` | `string` | The DocType name (set automatically) | +| `fields` | `string[]` | Fields to retrieve from the server | +| `filters` | `Record` | Filters for the query | +| `orderBy` | `string` | Order by clause (e.g., `` `tabUser`.`name` asc ``) | +| `limit` | `number` | Number of records to fetch | +| `offset` | `number` | Offset for pagination | +| `pageLength` | `number` | Alias for limit | +| `pageSize` | `number` | Alias for limit | + +## Advanced Usage + +### With Filters + +```tsx + +``` + +### With Ordering + +```tsx + +``` + +### With Pagination + +```tsx + +``` + +### Without Row Selection + +```tsx + +``` + +### Custom Loading Component + +```tsx +Loading users...} +/> +``` + +### Custom Empty State + +```tsx +No users found} +/> +``` + +### Custom Error Component + +```tsx + ( +
+

Error: {error.message}

+ +
+ )} +/> +``` + +### Manual Data Fetching + +```tsx +function MyApp() { + const { data, loading, error, fetch } = useFrappeGetDocList( + { + doctype: 'User', + fields: ['name', 'email'], + }, + false // Don't auto-fetch + ); + + return ( +
+ + +
+ ); +} +``` + +### With Callbacks + +```tsx + { + console.log('Data loaded:', data); + }} + onError={(error) => { + console.error('Error loading data:', error); + }} +/> +``` + +## Column Configuration + +Each column in the columns array should have the following structure: + +```tsx +{ + label: string; // Display label for the column header + key: string; // Data key from the document + width?: string | number; // Column width (e.g., '200px', 3) + getLabel?: (props: { row: any }) => ReactNode; // Custom label renderer + prefix?: (props: { row: any }) => ReactNode; // Prefix element before the value + suffix?: (props: { row: any }) => ReactNode; // Suffix element after the value +} +``` + +## useFrappeGetDocList Hook + +The hook can also be used independently: + +```tsx +import { useFrappeGetDocList } from '@rtcamp/frappe-ui-react'; + +function MyComponent() { + const { data, loading, error, fetch } = useFrappeGetDocList({ + doctype: 'User', + fields: ['name', 'email'], + filters: { enabled: 1 }, + limit: 20, + }); + + if (loading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return ( +
+ {data?.data?.map((user) => ( +
{user.name}
+ ))} +
+ ); +} +``` + +## Requirements + +- Frappe backend with REST API enabled +- Valid Frappe CSRF token available as `window.csrf_token` +- Proper CORS headers if using from a different domain + +## Error Handling + +The component provides built-in error handling with a retry button. You can also: + +1. Provide a custom error component +2. Use the `onError` callback +3. Access the error state through the hook directly + +## Performance Considerations + +- Use `fields` parameter to fetch only required fields +- Use `limit` parameter to paginate large datasets +- Use `filters` to reduce the dataset on the server side +- Consider using `auto={false}` for conditional fetching + +## Browser Compatibility + +Works in all modern browsers that support: +- ES2015+ +- React 19+ +- Fetch API + +## Related Components + +- `ListView` - Base table component +- `ListHeader` - Table header +- `ListRows` - Table rows +- `ListRow` - Single row +- `ListRowItem` - Row item/cell + +## Examples + +See the Storybook stories in `docTypeTable.stories.tsx` for more detailed examples and use cases. diff --git a/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.stories.tsx b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.stories.tsx new file mode 100644 index 00000000..7cb45d88 --- /dev/null +++ b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.stories.tsx @@ -0,0 +1,234 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Avatar } from "../avatar"; +import ListView from "../listview/listView"; + +const meta: Meta = { + title: "Components/DocTypeTable", + parameters: { + layout: "padded", + docs: { + description: { + component: ` +DocTypeTable is a general-purpose table component for displaying Frappe DocType documents. + +## Key Features +- **Automatic Data Fetching**: Fetches data from Frappe backend using useFrappeGetDocList hook +- **Transparent Parameter Passing**: All useFrappeGetDocList parameters flow directly through the component +- **Configurable Columns**: Define table structure with custom rendering +- **State Management**: Handles loading, error, and empty states +- **Selection Support**: Built-in row selection capability +- **Customizable Components**: Override loading, error, and empty states with custom components + +## Usage Example +\`\`\`tsx + + }, + { label: "Email", key: "email", width: "200px" }, + { label: "Type", key: "user_type" }, + ]} + params={{ + fields: ['name', 'email', 'user_type', 'enabled'], + filters: { enabled: 1 }, + limit: 20 + }} + rowKey="name" +/> +\`\`\` + +## Component Props +- \`doctype\`: Frappe DocType name (e.g., 'User', 'Employee') +- \`columns\`: Column configuration array +- \`params\`: Query parameters (fields, filters, orderBy, limit, offset) +- \`rowKey\`: Primary key field for identifying rows +- \`auto\`: Auto-fetch on mount (default: true) +- \`loadingComponent\`: Custom loading UI component +- \`emptyComponent\`: Custom empty state component +- \`errorComponent\`: Custom error component +- \`onDataLoad\`: Callback when data loads +- \`onError\`: Callback on error + `, + }, + }, + }, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +const columns = [ + { + label: "Name", + key: "name", + width: 3, + getLabel: ({ row }: { row: Record }) => row.name, + prefix: ({ row }: { row: Record }) => ( + + ), + }, + { + label: "Email", + key: "email", + width: "200px", + }, + { + label: "Type", + key: "user_type", + }, + { + label: "Enabled", + key: "enabled", + }, +]; + +const rows = [ + { + name: "John Doe", + email: "john@example.com", + user_type: "System User", + enabled: 1, + user_image: "https://i.pravatar.cc/150?img=1", + }, + { + name: "Jane Smith", + email: "jane@example.com", + user_type: "System User", + enabled: 1, + user_image: "https://i.pravatar.cc/150?img=2", + }, + { + name: "Bob Johnson", + email: "bob@example.com", + user_type: "Website User", + enabled: 0, + user_image: "https://i.pravatar.cc/150?img=3", + }, +]; + +export const Default: Story = { + render: (args) => ( + >} + rowKey={args.rowKey as string} + options={args.options} + /> + ), + args: { + rows, + rowKey: "name", + options: { + options: { + selectable: true, + }, + }, + }, +}; + +export const Loading: Story = { + render: () => ( +
+
+
+

Loading records...

+
+
+ ), +}; + +export const WithBadges: Story = { + render: (args) => { + const columnsWithBadges = [ + { + label: "Name", + key: "name", + width: 3, + getLabel: ({ row }: { row: Record }) => row.name, + prefix: ({ row }: { row: Record }) => ( + + ), + }, + { + label: "Email", + key: "email", + width: "200px", + }, + { + label: "Type", + key: "user_type", + }, + { + label: "Status", + key: "enabled", + getLabel: ({ row }: { row: Record }) => ( + + {row.enabled ? "Active" : "Inactive"} + + ), + }, + ]; + + return ( + >} + rowKey={args.rowKey as string} + options={args.options} + /> + ); + }, + args: { + rows, + rowKey: "name", + options: { + options: { + selectable: true, + }, + }, + }, +}; + +export const Empty: Story = { + render: () => ( +
+ + + +

No records found

+

Try adjusting your filters

+
+ ), +}; diff --git a/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx new file mode 100644 index 00000000..061ad6df --- /dev/null +++ b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx @@ -0,0 +1,186 @@ +import React, { HTMLAttributes, ReactNode } from "react"; +import ListView from "../listview/listView"; +import ListHeader from "../listview/listHeader"; +import ListRows from "../listview/listRows"; +import ListSelectBanner from "../listview/listSelectBanner"; +import LoadingIndicator from "../loadingIndicator"; +import { useFrappeGetDocList, type FrappeGetDocListParams } from "../hooks/useFrappeGetDocList"; +import { type ListOptionsProps } from "../listview/listContext"; + +export interface DocTypeTableProps extends Omit, 'onError'> { + /** + * The DocType name (e.g., 'User', 'Employee', 'Customer') + */ + doctype: string; + + /** + * Columns configuration for the table + */ + columns: Array>; + + /** + * The key field to use for row identification (e.g., 'name') + */ + rowKey?: string; + + /** + * Parameters to pass to useFrappeGetDocList + * This includes fields, filters, ordering, pagination, etc. + */ + params?: Omit; + + /** + * ListView options for customization + */ + options?: ListOptionsProps; + + /** + * Whether to automatically fetch data on mount + */ + auto?: boolean; + + /** + * Custom loading component + */ + loadingComponent?: ReactNode; + + /** + * Custom empty state component + */ + emptyComponent?: ReactNode; + + /** + * Custom error component or message + */ + errorComponent?: ReactNode | ((error: Error) => ReactNode); + + /** + * Callback when data is loaded + */ + onDataLoad?: (data: Array>) => void; + + /** + * Callback when error occurs + */ + onError?: (error: Error) => void; +} + +const DocTypeTable: React.FC = ({ + doctype, + columns, + rowKey = "name", + params = {}, + options, + auto = true, + loadingComponent, + emptyComponent, + errorComponent, + onDataLoad, + onError, + ...attrs +}) => { + // Combine params with doctype + const fullParams: FrappeGetDocListParams = { + doctype, + ...params, + }; + + // Use the useFrappeGetDocList hook + const { data, loading, error, fetch } = useFrappeGetDocList(fullParams, auto); + + // Call callbacks when data or error changes + React.useEffect(() => { + if (data?.data) { + onDataLoad?.(data.data); + } + }, [data?.data, onDataLoad]); + + React.useEffect(() => { + if (error) { + onError?.(error); + } + }, [error, onError]); + + // Show loading state + if (loading) { + return ( +
+ {loadingComponent || } +
+ ); + } + + // Show error state + if (error) { + return ( +
+ {typeof errorComponent === "function" + ? errorComponent(error) + : errorComponent || ( +
+

Error loading {doctype}

+

{error?.message}

+ +
+ )} +
+ ); + } + + const rows = data?.data || []; + + // Default table options + const defaultOptions: ListOptionsProps = { + options: { + selectable: true, + }, + }; + + const mergedOptions = { + ...defaultOptions, + ...options, + }; + + // Handle empty state + if (rows.length === 0) { + // If custom empty component provided, render it + if (emptyComponent) { + return ( +
+ {emptyComponent} +
+ ); + } + + // Otherwise render default empty state outside ListView + return ( +
+
+

No {doctype} records found

+
+
+ ); + } + + return ( +
+ + + + {mergedOptions?.options?.selectable && } + +
+ ); +}; + +export default DocTypeTable; diff --git a/packages/frappe-ui-react/src/components/docTypeTable/index.ts b/packages/frappe-ui-react/src/components/docTypeTable/index.ts new file mode 100644 index 00000000..f00583ba --- /dev/null +++ b/packages/frappe-ui-react/src/components/docTypeTable/index.ts @@ -0,0 +1 @@ +export { default as DocTypeTable, type DocTypeTableProps } from "./docTypeTable"; diff --git a/packages/frappe-ui-react/src/components/hooks/index.ts b/packages/frappe-ui-react/src/components/hooks/index.ts index 4a768a6e..a9444eb4 100644 --- a/packages/frappe-ui-react/src/components/hooks/index.ts +++ b/packages/frappe-ui-react/src/components/hooks/index.ts @@ -1,2 +1,3 @@ export * from "./useResource"; +export * from "./useFrappeGetDocList"; export { default as useWindowSize } from "./useWindowSize"; diff --git a/packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts b/packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts new file mode 100644 index 00000000..1c1ba3dd --- /dev/null +++ b/packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts @@ -0,0 +1,128 @@ +import { useEffect } from "react"; +import { useResource, type ResourceOutput } from "./useResource"; + +export interface FrappeGetDocListParams { + doctype: string; + fields?: string[]; + filters?: Record; + orderBy?: string; + limit?: number; + offset?: number; + pageLength?: number; + pageSize?: number; + [key: string]: unknown; +} + +export interface FrappeDocListResponse { + data: Array>; + total_count?: number; + keys?: string[]; +} + +/** + * Hook to fetch a list of Frappe documents + * This hook uses the useFrappeGetDocList API from Frappe backend + * + * @param params - Parameters for fetching the document list + * @param auto - Whether to automatically fetch on mount (default: true) + * @returns Object containing data, loading, error states and fetch method + * + * @example + * const { data, loading, error, fetch } = useFrappeGetDocList({ + * doctype: 'User', + * fields: ['name', 'email', 'user_type'], + * filters: { enabled: 1 }, + * limit: 20 + * }); + */ +export function useFrappeGetDocList( + params: FrappeGetDocListParams, + auto: boolean = true +): ResourceOutput { + // Build query parameters for the API call + const buildParams = () => { + const queryParams = new URLSearchParams(); + + if (params.fields) { + queryParams.append("fields", JSON.stringify(params.fields)); + } + + if (params.filters) { + queryParams.append("filters", JSON.stringify(params.filters)); + } + + if (params.orderBy) { + queryParams.append("order_by", params.orderBy); + } + + // Use limit as pageLength if not specified + const limit = params.limit || params.pageLength || params.pageSize || 20; + queryParams.append("limit_page_length", String(limit)); + + if (params.offset) { + queryParams.append("limit_page_length_offset", String(params.offset)); + } + + // Add any additional parameters + Object.keys(params).forEach((key) => { + if ( + ![ + "doctype", + "fields", + "filters", + "orderBy", + "limit", + "offset", + "pageLength", + "pageSize", + ].includes(key) + ) { + queryParams.append(key, String((params as Record)[key])); + } + }); + + return queryParams.toString(); + }; + + // Create the fetch function + const fetchDocList = async (): Promise => { + const queryString = buildParams(); + const url = `/api/resource/${params.doctype}?${queryString}`; + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Frappe-CSRF-Token": (window as Window & { csrf_token?: string }).csrf_token || "", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch ${params.doctype} list: ${response.statusText}` + ); + } + + const responseData = await response.json(); + + // Format the response to match our interface + return { + data: responseData.data || [], + total_count: responseData.total_count, + keys: responseData.keys, + }; + }; + + const resource = useResource(fetchDocList); + + // Auto-fetch on mount if auto is true + useEffect(() => { + if (auto) { + resource.fetch().catch(() => { + // Error is handled in the resource state + }); + } + }, [auto, params.doctype, resource]); + + return resource; +} diff --git a/packages/frappe-ui-react/src/components/index.ts b/packages/frappe-ui-react/src/components/index.ts index dc7053e8..829c5617 100644 --- a/packages/frappe-ui-react/src/components/index.ts +++ b/packages/frappe-ui-react/src/components/index.ts @@ -13,6 +13,7 @@ export * from "./commandPalette"; export * from "./datePicker"; export * from "./dialog"; export * from "./divider"; +export * from "./docTypeTable"; export * from "./dropdown"; export * from "./errorMessage"; export * from "./fileUploader"; From e0c0973afcb26b5695f0998925e3caf86f81100d Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sun, 18 Jan 2026 11:37:30 +0530 Subject: [PATCH 2/5] Update packages/frappe-ui-react/src/components/docTypeTable/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/frappe-ui-react/src/components/docTypeTable/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frappe-ui-react/src/components/docTypeTable/README.md b/packages/frappe-ui-react/src/components/docTypeTable/README.md index e1d06da8..cf0f96bb 100644 --- a/packages/frappe-ui-react/src/components/docTypeTable/README.md +++ b/packages/frappe-ui-react/src/components/docTypeTable/README.md @@ -81,7 +81,7 @@ Parameters passed to the API call: | `doctype` | `string` | The DocType name (set automatically) | | `fields` | `string[]` | Fields to retrieve from the server | | `filters` | `Record` | Filters for the query | -| `orderBy` | `string` | Order by clause (e.g., `` `tabUser`.`name` asc ``) | +| `orderBy` | `string` | Order by clause (e.g., ``'`tabUser`.`name` asc'``) | | `limit` | `number` | Number of records to fetch | | `offset` | `number` | Offset for pagination | | `pageLength` | `number` | Alias for limit | From 3b4e6f8736c026c3cb6113100e2eed24183f87d4 Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sun, 18 Jan 2026 11:38:24 +0530 Subject: [PATCH 3/5] Update packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/components/hooks/useFrappeGetDocList.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts b/packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts index 1c1ba3dd..53489ac3 100644 --- a/packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts +++ b/packages/frappe-ui-react/src/components/hooks/useFrappeGetDocList.ts @@ -87,8 +87,13 @@ export function useFrappeGetDocList( // Create the fetch function const fetchDocList = async (): Promise => { const queryString = buildParams(); - const url = `/api/resource/${params.doctype}?${queryString}`; + const doctype = params.doctype; + if (!/^[A-Za-z0-9_]+$/.test(doctype)) { + throw new Error("Invalid doctype parameter"); + } + + const url = `/api/resource/${doctype}?${queryString}`; const response = await fetch(url, { method: "GET", headers: { From 0b358c39286568ed569927b46e5f6944bf872798 Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sun, 18 Jan 2026 11:40:35 +0530 Subject: [PATCH 4/5] Update packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/components/docTypeTable/docTypeTable.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx index 061ad6df..16787fe1 100644 --- a/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx +++ b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx @@ -104,7 +104,12 @@ const DocTypeTable: React.FC = ({ // Show loading state if (loading) { return ( -
+
{loadingComponent || }
); From c4baecd99935933a818d0999e0f58a21923209fa Mon Sep 17 00:00:00 2001 From: Akshat Shukla Date: Sun, 18 Jan 2026 11:45:31 +0530 Subject: [PATCH 5/5] Update packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/docTypeTable/docTypeTable.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx index 16787fe1..dbf55b05 100644 --- a/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx +++ b/packages/frappe-ui-react/src/components/docTypeTable/docTypeTable.tsx @@ -88,18 +88,30 @@ const DocTypeTable: React.FC = ({ // Use the useFrappeGetDocList hook const { data, loading, error, fetch } = useFrappeGetDocList(fullParams, auto); + // Store latest callback props in refs to avoid unnecessary effect executions + const onDataLoadRef = React.useRef(); + const onErrorRef = React.useRef(); + + React.useEffect(() => { + onDataLoadRef.current = onDataLoad; + }, [onDataLoad]); + + React.useEffect(() => { + onErrorRef.current = onError; + }, [onError]); + // Call callbacks when data or error changes React.useEffect(() => { if (data?.data) { - onDataLoad?.(data.data); + onDataLoadRef.current?.(data.data); } - }, [data?.data, onDataLoad]); + }, [data?.data]); React.useEffect(() => { if (error) { - onError?.(error); + onErrorRef.current?.(error); } - }, [error, onError]); + }, [error]); // Show loading state if (loading) {