Skip to content

Commit 1c94f96

Browse files
committed
feat(ObjectGrid): optimize data loading and schema handling to prevent infinite re-renders
1 parent b54da1b commit 1c94f96

1 file changed

Lines changed: 103 additions & 94 deletions

File tree

packages/plugin-grid/src/ObjectGrid.tsx

Lines changed: 103 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
151151

152152
const hasInlineData = dataConfig?.provider === 'value';
153153

154+
// Extract stable primitive/reference-stable values from schema for dependency arrays.
155+
// This prevents infinite re-render loops when schema is a new object on each render
156+
// (e.g. when rendered through SchemaRenderer which creates a fresh evaluatedSchema).
157+
const objectName = dataConfig?.provider === 'object' && dataConfig && 'object' in dataConfig
158+
? (dataConfig as any).object
159+
: schema.objectName;
160+
const schemaFields = schema.fields;
161+
const schemaColumns = schema.columns;
162+
const schemaFilter = schema.filter;
163+
const schemaSort = schema.sort;
164+
const schemaPagination = schema.pagination;
165+
const schemaPageSize = schema.pageSize;
166+
167+
// --- Inline data effect (synchronous, no fetch needed) ---
154168
useEffect(() => {
155169
if (hasInlineData && dataConfig?.provider === 'value') {
156170
// Only update if data is different to avoid infinite loop
@@ -165,42 +179,104 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
165179
}
166180
}, [hasInlineData, dataConfig]);
167181

182+
// --- Unified async data loading effect ---
183+
// Combines schema fetch + data fetch into a single async flow with AbortController.
184+
// This avoids the fragile "chained effects" pattern where Effect 1 sets objectSchema,
185+
// triggering Effect 2 to call fetchData — a pattern prone to infinite loops when
186+
// fetchData's reference is unstable.
168187
useEffect(() => {
169-
const fetchObjectSchema = async () => {
188+
if (hasInlineData) return;
189+
190+
let cancelled = false;
191+
192+
const loadSchemaAndData = async () => {
193+
setLoading(true);
194+
setError(null);
170195
try {
171-
if (!dataSource) {
196+
// --- Step 1: Resolve object schema ---
197+
let resolvedSchema: any = null;
198+
const cols = normalizeColumns(schemaColumns) || schemaFields;
199+
200+
if (cols && objectName) {
201+
// We have explicit columns — use a minimal schema stub
202+
resolvedSchema = { name: objectName, fields: {} };
203+
} else if (objectName && dataSource) {
204+
// Fetch full schema from DataSource
205+
const schemaData = await dataSource.getObjectSchema(objectName);
206+
if (cancelled) return;
207+
resolvedSchema = schemaData;
208+
} else if (!objectName) {
209+
throw new Error('Object name required for data fetching');
210+
} else {
172211
throw new Error('DataSource required');
173212
}
174-
175-
// For object provider, get the object name
176-
const objectName = dataConfig?.provider === 'object' && 'object' in dataConfig
177-
? dataConfig.object
178-
: schema.objectName;
179-
180-
if (!objectName) {
181-
throw new Error('Object name required for object provider');
213+
214+
if (!cancelled) {
215+
setObjectSchema(resolvedSchema);
216+
}
217+
218+
// --- Step 2: Fetch data ---
219+
if (dataSource && objectName) {
220+
const getSelectFields = () => {
221+
if (schemaFields) return schemaFields;
222+
if (schemaColumns && Array.isArray(schemaColumns)) {
223+
return schemaColumns.map((c: any) => typeof c === 'string' ? c : c.field);
224+
}
225+
return undefined;
226+
};
227+
228+
const params: any = {
229+
$select: getSelectFields(),
230+
$top: (schemaPagination as any)?.pageSize || schemaPageSize || 50,
231+
};
232+
233+
// Support new filter format
234+
if (schemaFilter && Array.isArray(schemaFilter)) {
235+
params.$filter = schemaFilter;
236+
} else if (schema.defaultFilters) {
237+
// Legacy support
238+
params.$filter = schema.defaultFilters;
239+
}
240+
241+
// Support new sort format
242+
if (schemaSort) {
243+
if (typeof schemaSort === 'string') {
244+
params.$orderby = schemaSort;
245+
} else if (Array.isArray(schemaSort)) {
246+
params.$orderby = schemaSort
247+
.map((s: any) => `${s.field} ${s.order}`)
248+
.join(', ');
249+
}
250+
} else if (schema.defaultSort) {
251+
// Legacy support
252+
params.$orderby = `${(schema.defaultSort as any).field} ${(schema.defaultSort as any).order}`;
253+
}
254+
255+
const result = await dataSource.find(objectName, params);
256+
if (cancelled) return;
257+
setData(result.data || []);
182258
}
183-
184-
const schemaData = await dataSource.getObjectSchema(objectName);
185-
setObjectSchema(schemaData);
186259
} catch (err) {
187-
setError(err as Error);
260+
if (!cancelled) {
261+
setError(err as Error);
262+
}
263+
} finally {
264+
if (!cancelled) {
265+
setLoading(false);
266+
}
188267
}
189268
};
190269

191-
// Normalize columns (support both legacy 'fields' and new 'columns')
192-
const cols = normalizeColumns(schema.columns) || schema.fields;
193-
194-
if (hasInlineData && cols) {
195-
setObjectSchema({ name: schema.objectName, fields: {} });
196-
} else if (schema.objectName && !hasInlineData && dataSource) {
197-
fetchObjectSchema();
198-
}
199-
}, [schema.objectName, schema.columns, schema.fields, dataSource, hasInlineData, dataConfig]);
270+
loadSchemaAndData();
271+
272+
return () => {
273+
cancelled = true;
274+
};
275+
}, [objectName, schemaFields, schemaColumns, schemaFilter, schemaSort, schemaPagination, schemaPageSize, dataSource, hasInlineData, dataConfig]);
200276

201277
const generateColumns = useCallback(() => {
202278
// Use normalized columns (support both new and legacy)
203-
const cols = normalizeColumns(schema.columns);
279+
const cols = normalizeColumns(schemaColumns);
204280

205281
if (cols) {
206282
// Check if columns are already in data-table format (have 'accessorKey')
@@ -243,7 +319,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
243319
if (hasInlineData) {
244320
const inlineData = dataConfig?.provider === 'value' ? dataConfig.items as any[] : [];
245321
if (inlineData.length > 0) {
246-
const fieldsToShow = schema.fields || Object.keys(inlineData[0]);
322+
const fieldsToShow = schemaFields || Object.keys(inlineData[0]);
247323
return fieldsToShow.map((fieldName) => ({
248324
header: fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/_/g, ' '),
249325
accessorKey: fieldName,
@@ -254,7 +330,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
254330
if (!objectSchema) return [];
255331

256332
const generatedColumns: any[] = [];
257-
const fieldsToShow = schema.fields || Object.keys(objectSchema.fields || {});
333+
const fieldsToShow = schemaFields || Object.keys(objectSchema.fields || {});
258334

259335
fieldsToShow.forEach((fieldName) => {
260336
const field = objectSchema.fields?.[fieldName];
@@ -272,74 +348,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
272348
});
273349

274350
return generatedColumns;
275-
}, [objectSchema, schema.fields, schema.columns, dataConfig, hasInlineData]);
276-
277-
const fetchData = useCallback(async () => {
278-
if (hasInlineData || !dataSource) return;
279-
280-
setLoading(true);
281-
try {
282-
// Get object name from data config or schema
283-
const objectName = dataConfig?.provider === 'object' && 'object' in dataConfig
284-
? dataConfig.object
285-
: schema.objectName;
286-
287-
if (!objectName) {
288-
throw new Error('Object name required for data fetching');
289-
}
290-
291-
// Helper to get select fields
292-
const getSelectFields = () => {
293-
if (schema.fields) return schema.fields;
294-
if (schema.columns && Array.isArray(schema.columns)) {
295-
return schema.columns.map(c => typeof c === 'string' ? c : c.field);
296-
}
297-
return undefined;
298-
};
299-
300-
const params: any = {
301-
$select: getSelectFields(),
302-
$top: schema.pagination?.pageSize || schema.pageSize || 50,
303-
};
304-
305-
// Support new filter format
306-
if (schema.filter && Array.isArray(schema.filter)) {
307-
params.$filter = schema.filter;
308-
} else if ('defaultFilters' in schema && schema.defaultFilters) {
309-
// Legacy support
310-
params.$filter = schema.defaultFilters;
311-
}
312-
313-
// Support new sort format
314-
if (schema.sort) {
315-
if (typeof schema.sort === 'string') {
316-
// Legacy string format
317-
params.$orderby = schema.sort;
318-
} else if (Array.isArray(schema.sort)) {
319-
// New array format
320-
params.$orderby = schema.sort
321-
.map(s => `${s.field} ${s.order}`)
322-
.join(', ');
323-
}
324-
} else if ('defaultSort' in schema && schema.defaultSort) {
325-
// Legacy support
326-
params.$orderby = `${schema.defaultSort.field} ${schema.defaultSort.order}`;
327-
}
328-
329-
const result = await dataSource.find(objectName, params);
330-
setData(result.data || []);
331-
} catch (err) {
332-
setError(err as Error);
333-
} finally {
334-
setLoading(false);
335-
}
336-
}, [schema, dataSource, hasInlineData, dataConfig]);
337-
338-
useEffect(() => {
339-
if (objectSchema || hasInlineData) {
340-
fetchData();
341-
}
342-
}, [objectSchema, hasInlineData, fetchData]);
351+
}, [objectSchema, schemaFields, schemaColumns, dataConfig, hasInlineData]);
343352

344353
if (error) {
345354
return (

0 commit comments

Comments
 (0)