@@ -473,10 +473,12 @@ describe('dehydration and rehydration', () => {
473
473
const serverAddTodo = vi
474
474
. fn ( )
475
475
. mockImplementation ( ( ) => Promise . reject ( new Error ( 'offline' ) ) )
476
- const serverOnMutate = vi . fn ( ) . mockImplementation ( ( variables ) => {
477
- const optimisticTodo = { id : 1 , text : variables . text }
478
- return { optimisticTodo }
479
- } )
476
+ const serverOnMutate = vi
477
+ . fn ( )
478
+ . mockImplementation ( ( variables : { text : string } ) => {
479
+ const optimisticTodo = { id : 1 , text : variables . text }
480
+ return { optimisticTodo }
481
+ } )
480
482
const serverOnSuccess = vi . fn ( )
481
483
482
484
const serverClient = new QueryClient ( )
@@ -511,13 +513,17 @@ describe('dehydration and rehydration', () => {
511
513
const parsed = JSON . parse ( stringified )
512
514
const client = new QueryClient ( )
513
515
514
- const clientAddTodo = vi . fn ( ) . mockImplementation ( ( variables ) => {
515
- return { id : 2 , text : variables . text }
516
- } )
517
- const clientOnMutate = vi . fn ( ) . mockImplementation ( ( variables ) => {
518
- const optimisticTodo = { id : 1 , text : variables . text }
519
- return { optimisticTodo }
520
- } )
516
+ const clientAddTodo = vi
517
+ . fn ( )
518
+ . mockImplementation ( ( variables : { text : string } ) => {
519
+ return { id : 2 , text : variables . text }
520
+ } )
521
+ const clientOnMutate = vi
522
+ . fn ( )
523
+ . mockImplementation ( ( variables : { text : string } ) => {
524
+ const optimisticTodo = { id : 1 , text : variables . text }
525
+ return { optimisticTodo }
526
+ } )
521
527
const clientOnSuccess = vi . fn ( )
522
528
523
529
client . setMutationDefaults ( [ 'addTodo' ] , {
@@ -1116,6 +1122,60 @@ describe('dehydration and rehydration', () => {
1116
1122
serverQueryClient . clear ( )
1117
1123
} )
1118
1124
1125
+ test ( 'should not overwrite query in cache if existing query is newer (with promise)' , async ( ) => {
1126
+ // --- server ---
1127
+
1128
+ const serverQueryClient = new QueryClient ( {
1129
+ defaultOptions : {
1130
+ dehydrate : {
1131
+ shouldDehydrateQuery : ( ) => true ,
1132
+ } ,
1133
+ } ,
1134
+ } )
1135
+
1136
+ const promise = serverQueryClient . prefetchQuery ( {
1137
+ queryKey : [ 'data' ] ,
1138
+ queryFn : async ( ) => {
1139
+ await sleep ( 10 )
1140
+ return 'server data'
1141
+ } ,
1142
+ } )
1143
+
1144
+ const dehydrated = dehydrate ( serverQueryClient )
1145
+
1146
+ await vi . advanceTimersByTimeAsync ( 10 )
1147
+ await promise
1148
+
1149
+ // Pretend the output of this server part is cached for a long time
1150
+
1151
+ // --- client ---
1152
+
1153
+ await vi . advanceTimersByTimeAsync ( 10_000 ) // Arbitrary time in the future
1154
+
1155
+ const clientQueryClient = new QueryClient ( )
1156
+
1157
+ clientQueryClient . setQueryData ( [ 'data' ] , 'newer data' , {
1158
+ updatedAt : Date . now ( ) ,
1159
+ } )
1160
+
1161
+ hydrate ( clientQueryClient , dehydrated )
1162
+
1163
+ // If the query was hydrated in error, it would still take some time for it
1164
+ // to end up in the cache, so for the test to fail properly on regressions,
1165
+ // wait for the fetchStatus to be idle
1166
+ await vi . waitFor ( ( ) =>
1167
+ expect ( clientQueryClient . getQueryState ( [ 'data' ] ) ?. fetchStatus ) . toBe (
1168
+ 'idle' ,
1169
+ ) ,
1170
+ )
1171
+ await vi . waitFor ( ( ) =>
1172
+ expect ( clientQueryClient . getQueryData ( [ 'data' ] ) ) . toBe ( 'newer data' ) ,
1173
+ )
1174
+
1175
+ clientQueryClient . clear ( )
1176
+ serverQueryClient . clear ( )
1177
+ } )
1178
+
1119
1179
test ( 'should overwrite data when a new promise is streamed in' , async ( ) => {
1120
1180
const serializeDataMock = vi . fn ( ( data : any ) => data )
1121
1181
const deserializeDataMock = vi . fn ( ( data : any ) => data )
@@ -1291,4 +1351,55 @@ describe('dehydration and rehydration', () => {
1291
1351
process . env . NODE_ENV = originalNodeEnv
1292
1352
consoleMock . mockRestore ( )
1293
1353
} )
1354
+
1355
+ // When React hydrates promises across RSC/client boundaries, it passes
1356
+ // them as special ReactPromise types. There are situations where the
1357
+ // promise might have time to resolve before we end up hydrating it, in
1358
+ // which case React will have made it a special synchronous thenable where
1359
+ // .then() resolves immediately.
1360
+ // In these cases it's important we hydrate the data synchronously, or else
1361
+ // the data in the cache wont match the content that was rendered on the server.
1362
+ // What can end up happening otherwise is that the content is visible from the
1363
+ // server, but the client renders a Suspense fallback, only to immediately show
1364
+ // the data again.
1365
+ test ( 'should rehydrate synchronous thenable immediately' , async ( ) => {
1366
+ // --- server ---
1367
+
1368
+ const serverQueryClient = new QueryClient ( {
1369
+ defaultOptions : {
1370
+ dehydrate : {
1371
+ shouldDehydrateQuery : ( ) => true ,
1372
+ } ,
1373
+ } ,
1374
+ } )
1375
+ const originalPromise = serverQueryClient . prefetchQuery ( {
1376
+ queryKey : [ 'data' ] ,
1377
+ queryFn : ( ) => null ,
1378
+ } )
1379
+
1380
+ const dehydrated = dehydrate ( serverQueryClient )
1381
+
1382
+ // --- server end ---
1383
+
1384
+ // Simulate a synchronous thenable
1385
+ // @ts -expect-error
1386
+ dehydrated . queries [ 0 ] . promise . then = ( cb ) => {
1387
+ cb ?.( 'server data' )
1388
+ }
1389
+
1390
+ // --- client ---
1391
+
1392
+ const clientQueryClient = new QueryClient ( )
1393
+ hydrate ( clientQueryClient , dehydrated )
1394
+
1395
+ // If data is already resolved, it should end up in the cache immediately
1396
+ expect ( clientQueryClient . getQueryData ( [ 'data' ] ) ) . toBe ( 'server data' )
1397
+
1398
+ // Need to await the original promise or else it will get a cancellation
1399
+ // error and test will fail
1400
+ await originalPromise
1401
+
1402
+ clientQueryClient . clear ( )
1403
+ serverQueryClient . clear ( )
1404
+ } )
1294
1405
} )
0 commit comments