@@ -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