@@ -209,120 +209,172 @@ export const createUnionAccessor = ({
209209} ;
210210
211211/**
212- * Globally defining the proxy handler to avoid accidential scope references.
212+ * Creates a proxy handler for object accessors.
213+ *
214+ * Key fix for React 19 dev mode: For data-backed proxies, `ownKeys()` only
215+ * returns keys that exist in the cache, not all schema fields. This prevents
216+ * React's prop diffing from triggering selections for fields the user never
217+ * requested. The `allowEnumeration` context flag overrides this behavior for
218+ * helpers like selectFields() that need to enumerate all schema fields.
213219 */
214- const objectProxyHandler : ProxyHandler < GeneratedSchemaObject > = {
215- get ( currentType : Record < string , Type | undefined > , key , proxy ) {
216- if ( typeof key !== 'string' ) return ;
217-
218- if ( key === 'toJSON' ) {
219- return ( ) => {
220- const data = $meta ( proxy ) ?. cache . data ;
221-
222- if ( typeof data !== 'object' || data === null ) {
223- return data ;
224- }
220+ const createObjectProxyHandler = (
221+ data : CacheObject | undefined ,
222+ context : { activeEnumerators : number }
223+ ) : ProxyHandler < GeneratedSchemaObject > => {
224+ return {
225+ ownKeys ( target ) {
226+ // When allowEnumeration > 0 (e.g., selectFields helper), always
227+ // return all schema keys regardless of cache state.
228+ if ( context . activeEnumerators > 0 ) {
229+ return Reflect . ownKeys ( target ) . filter (
230+ ( k ) => typeof k === 'string'
231+ ) as string [ ] ;
232+ }
225233
226- return Object . entries ( data ) . reduce < Record < string , unknown > > (
227- ( prev , [ key , value ] ) => {
228- if ( ! isSkeleton ( value ) ) {
229- prev [ key ] = value ;
230- }
234+ // For data-backed proxies with actual data, only return keys that exist
235+ // in the cache. This prevents React 19's prop diffing from seeing all
236+ // schema fields when it enumerates a proxy - it only sees the actually
237+ // cached fields.
238+ //
239+ // For skeleton proxies (no data or empty data), return all schema keys
240+ // so that spread and for...in work correctly for initial selections.
241+ const dataKeys = data
242+ ? ( Reflect . ownKeys ( data ) . filter (
243+ ( k ) => typeof k === 'string'
244+ ) as string [ ] )
245+ : [ ] ;
246+
247+ if ( dataKeys . length > 0 ) {
248+ return dataKeys ;
249+ }
231250
232- return prev ;
233- } ,
234- { }
251+ return Reflect . ownKeys ( target ) . filter (
252+ ( k ) => typeof k === 'string'
253+ ) as string [ ] ;
254+ } ,
255+ getOwnPropertyDescriptor ( target , key ) {
256+ // For data-backed proxies, check data first
257+ if ( data ) {
258+ return (
259+ Reflect . getOwnPropertyDescriptor ( data , key ) ??
260+ Reflect . getOwnPropertyDescriptor ( target , key )
235261 ) ;
236- } ;
237- }
262+ }
238263
239- const meta = $meta ( proxy ) ;
240- if ( ! meta ) return ;
264+ return Reflect . getOwnPropertyDescriptor ( target , key ) ;
265+ } ,
266+ get ( currentType : Record < string , Type | undefined > , key , proxy ) {
267+ if ( typeof key !== 'string' ) return ;
268+
269+ if ( key === 'toJSON' ) {
270+ return ( ) => {
271+ const data = $meta ( proxy ) ?. cache . data ;
272+
273+ if ( typeof data !== 'object' || data === null ) {
274+ return data ;
275+ }
276+
277+ return Object . entries ( data ) . reduce < Record < string , unknown > > (
278+ ( prev , [ key , value ] ) => {
279+ if ( ! isSkeleton ( value ) ) {
280+ prev [ key ] = value ;
281+ }
282+
283+ return prev ;
284+ } ,
285+ { }
286+ ) ;
287+ } ;
288+ }
241289
242- if (
243- // Skip Query, Mutation and Subscription
244- meta . selection . parent !== undefined &&
245- // Prevent infinite recursions
246- ! getIdentityFields ( meta ) . includes ( key )
247- ) {
248- selectIdentityFields ( proxy , currentType ) ;
249- }
290+ const meta = $meta ( proxy ) ;
291+ if ( ! meta ) return ;
250292
251- const targetType = currentType [ key ] ;
252- if ( ! targetType || typeof targetType !== 'object' ) return ;
253-
254- const { __args, __type } = targetType ;
255- if ( __args ) {
256- return ( args ?: Record < string , unknown > ) =>
257- resolve (
258- proxy ,
259- meta . selection . getChild (
260- key ,
261- args ? { input : { types : __args ! , values : args } } : { }
262- ) ,
263- __type
264- ) ;
265- }
293+ if (
294+ // Skip Query, Mutation and Subscription
295+ meta . selection . parent !== undefined &&
296+ // Prevent infinite recursions
297+ ! getIdentityFields ( meta ) . includes ( key )
298+ ) {
299+ selectIdentityFields ( proxy , currentType ) ;
300+ }
266301
267- return resolve ( proxy , meta . selection . getChild ( key ) , __type ) ;
268- } ,
269- set ( _ , key , value , proxy ) {
270- const meta = $meta ( proxy ) ;
271- if ( typeof key !== 'string' || ! meta ) return false ;
302+ const targetType = currentType [ key ] ;
303+ if ( ! targetType || typeof targetType !== 'object' ) return ;
304+
305+ const { __args, __type } = targetType ;
306+ if ( __args ) {
307+ return ( args ?: Record < string , unknown > ) =>
308+ resolve (
309+ proxy ,
310+ meta . selection . getChild (
311+ key ,
312+ args ? { input : { types : __args ! , values : args } } : { }
313+ ) ,
314+ __type
315+ ) ;
316+ }
272317
273- const { cache, context, selection } = meta ;
318+ return resolve ( proxy , meta . selection . getChild ( key ) , __type ) ;
319+ } ,
320+ set ( _ , key , value , proxy ) {
321+ const meta = $meta ( proxy ) ;
322+ if ( typeof key !== 'string' || ! meta ) return false ;
274323
275- // Extract proxy data, keep the object reference unless users deep clone it.
276- value = deepMetadata ( value ) ?? value ;
324+ const { cache, context, selection } = meta ;
277325
278- if ( selection . ancestry . length <= 2 ) {
279- const [ type , field ] = selection . cacheKeys ;
326+ // Extract proxy data, keep the object reference unless users deep clone it.
327+ value = deepMetadata ( value ) ?? value ;
280328
281- if ( field ) {
282- const data =
283- context . cache . get ( `${ type } .${ field } ` , context . cacheOptions ) ?. data ??
284- { } ;
329+ if ( selection . ancestry . length <= 2 ) {
330+ const [ type , field ] = selection . cacheKeys ;
285331
286- if ( ! isPlainObject ( data ) ) return false ;
332+ if ( field ) {
333+ const data =
334+ context . cache . get ( `${ type } .${ field } ` , context . cacheOptions ) ?. data ??
335+ { } ;
287336
288- data [ key ] = value ;
337+ if ( ! isPlainObject ( data ) ) return false ;
289338
290- context . cache . set ( { [ type ] : { [ field ] : data } } ) ;
291- } else {
292- context . cache . set ( { [ type ] : { [ key ] : value } } ) ;
293- }
294- }
339+ data [ key ] = value ;
295340
296- let result = false ;
341+ context . cache . set ( { [ type ] : { [ field ] : data } } ) ;
342+ } else {
343+ context . cache . set ( { [ type ] : { [ key ] : value } } ) ;
344+ }
345+ }
297346
298- if ( isCacheObject ( cache . data ) ) {
299- result = Reflect . set ( cache . data , key , value ) ;
300- }
347+ let result = false ;
301348
302- /**
303- * Ported for backward compatability.
304- *
305- * Triggering selections via optimistic updates is asking for infinite
306- * recursions, also it's unnecessarily complicated to infer arrays,
307- * interfaces and union selections down the selection tree.
308- *
309- * If we can't figure out an elegant way to infer selections in future
310- * iterations, remove it at some point.
311- */
312- for ( const [ keys , scalar ] of flattenObject ( value ) ) {
313- let currentSelection = selection . getChild ( key ) ;
314- for ( const key of keys ) {
315- // Skip array indices
316- if ( ! isNaN ( Number ( key ) ) ) continue ;
317-
318- currentSelection = currentSelection . getChild ( key ) ;
349+ if ( isCacheObject ( cache . data ) ) {
350+ result = Reflect . set ( cache . data , key , value ) ;
319351 }
320352
321- context . select ( currentSelection , { ...cache , data : scalar } ) ;
322- }
353+ /**
354+ * Ported for backward compatability.
355+ *
356+ * Triggering selections via optimistic updates is asking for infinite
357+ * recursions, also it's unnecessarily complicated to infer arrays,
358+ * interfaces and union selections down the selection tree.
359+ *
360+ * If we can't figure out an elegant way to infer selections in future
361+ * iterations, remove it at some point.
362+ */
363+ for ( const [ keys , scalar ] of flattenObject ( value ) ) {
364+ let currentSelection = selection . getChild ( key ) ;
365+ for ( const key of keys ) {
366+ // Skip array indices
367+ if ( ! isNaN ( Number ( key ) ) ) continue ;
368+
369+ currentSelection = currentSelection . getChild ( key ) ;
370+ }
323371
324- return result ;
325- } ,
372+ context . select ( currentSelection , { ...cache , data : scalar } ) ;
373+ }
374+
375+ return result ;
376+ } ,
377+ } ;
326378} ;
327379
328380export type AccessorOptions = {
@@ -334,6 +386,7 @@ export const createObjectAccessor = <TSchemaType extends GeneratedSchemaObject>(
334386) => {
335387 const {
336388 cache : { data } ,
389+ context,
337390 context : { schema } ,
338391 type : { __type } ,
339392 } = meta ;
@@ -349,16 +402,17 @@ export const createObjectAccessor = <TSchemaType extends GeneratedSchemaObject>(
349402 const type = schema [ parseSchemaType ( __type ) . pureType ] ;
350403 if ( ! type ) throw new GQtyError ( `Invalid schema type ${ __type } .` ) ;
351404
405+ // Create a per-proxy handler
406+ // Pass context for allowEnumeration flag access
407+ const handler = createObjectProxyHandler (
408+ isCacheObject ( data ) ? data : undefined ,
409+ context
410+ ) ;
411+
352412 const proxy = new Proxy (
353413 // `type` here for ownKeys proxy trap
354414 type as TSchemaType ,
355- data
356- ? Object . assign ( { } , objectProxyHandler , {
357- getOwnPropertyDescriptor : ( target , key ) =>
358- Reflect . getOwnPropertyDescriptor ( data , key ) ??
359- Reflect . getOwnPropertyDescriptor ( target , key ) ,
360- } satisfies typeof objectProxyHandler )
361- : objectProxyHandler
415+ handler
362416 ) ;
363417
364418 $meta . set ( proxy , meta ) ;
0 commit comments