Skip to content

web: disable TanStack Query structural sharing#2972

Merged
dgdavid merged 2 commits intoapi-v2from
api-v2-disable-structural-sharing
Dec 23, 2025
Merged

web: disable TanStack Query structural sharing#2972
dgdavid merged 2 commits intoapi-v2from
api-v2-disable-structural-sharing

Conversation

@dgdavid
Copy link
Copy Markdown
Contributor

@dgdavid dgdavid commented Dec 23, 2025

TL;DR

Fixes ProgressBackdrop infinite blocking by disabling structural sharing in QueryClient default options.

Problem

Recently introduced ProgressBackdrop component uses a custom useTrackQueriesRefetch hook that subscribes to the QueryCache to detect when specific queries have been refetched. This allows the UI to unblock in the right moment when a progress finishes and the data for render the resulting interface is really ready.

However, TanStack Query's structural sharing optimization prevents QueryCache events from being emitted when refetched data is identical to the previous data. When structural sharing is enabled (the default), the library reuses the previous data reference for deeply equal results, which means no updated event is fired. This makes it impossible for our subscription to detect that a refetch occurred, breaking the progress tracking functionality.

This issue was discovered on the storage proposal page. While validation issues prevent a storage proposal, the API will return an identical response (a proposal object without storage data) on each refetch (if nothing else has changed in other areas). Since the response remains unchanged and no updated event is fired, ProgressBackdrop never detects the refetch completion and blocks the UI indefinitely if the user was not able to fix the issue in the first attempt to produce a different proposal. The same pattern is expected in other areas where the API response is identical to previous after a ProgressBackdrop block.

Solution

Disable structural sharing globally for all queries by setting structuralSharing: false globally in the QueryClient default options. This ensures that QueryCache subscriptions always receive updated events when queries refetch, regardless of whether the data changed.

The performance impact is expected to be minimal:

  • Identical data responses should be not that common
  • React reconciliation is efficient and only applies DOM updates when rendered output differs. Additionally, components should use useMemo/useCallback to prevent heavy computations from re-executing when data hasn't changed

Alternatives Considered

Several alternatives were evaluated but rejected as unnecessarily complex:

  • Adding artificial timestamps to JSON responses: it achieves nearly the same result but breaks types and pollutes the data model
  • Reverting to query observer pattern: more complex and more subscriptions with no clear benefit
  • Manual notifyOnChangeProps configuration: requires per-query or global configuration overhead, defeating the more convenient "tracked" default.
  • Separate tracker queries: a no go because increases not only the complexity but the maintenance.

Additional changes

Minor issues in tests has been fixed as part of this PR.

@dgdavid dgdavid requested review from ancorgs and imobachgs December 23, 2025 14:39
Structural sharing is disabled to ensure QueryCache subscriptions
receive 'updated' events even when refetched data is identical to
previous data.

With structural sharing enabled (default), TanStack Query reuses the
previous data reference when new data is deeply equal, preventing the
QueryCache from emitting update events. This makes it impossible to
detect refetches via subscriptions when data hasn't changed.

The custom useTrackQueriesRefetch hook (used by ProgressBackdrop) relies
on these events to detect when queries have been refetched, enabling it
to unblock the UI at the precise moment after a progress signal
completes.  Without these events, the UI cannot reliably determine when
to unblock.

The performance impact of disabling this optimization is expected to be
minimal because:

  * Identical data responses are relatively rare in practice
  * React's reconciliation efficiently skips DOM updates when rendered
    output is identical, even if components re-render. Any heavy
computations can be memoized to avoid re-execution when data hasn't
changed

Several alternatives were evaluated and rejected as unnecessarily
complex for the value provided:

  * Adding artificial timestamps to JSON responses (which achieves
    nearly the same result as disabling this optimization, but at the
cost of breaking types and polluting the data model)
  * Reverting useTrackQueriesRefetch to query observer pattern
  * Manually configuring notifyOnChangeProps globally or per-query
  * Implementing separate tracker queries alongside data queries

This approach simply trades one render optimization for simpler, more
reliable event detection.
Delete a no longer suitable test for ProgressBackdrop after making
`scope` mandatory in previous commits.

To cover a similar use case, add two simple unit tests at core/Page
after conditionally mounting the ProgressBackdrop when progress prop is
present
@dgdavid dgdavid force-pushed the api-v2-disable-structural-sharing branch from 82e3792 to cf9da48 Compare December 23, 2025 14:47
@dgdavid dgdavid requested a review from imobachgs December 23, 2025 15:04
@dgdavid dgdavid merged commit 4267caa into api-v2 Dec 23, 2025
7 of 9 checks passed
@dgdavid dgdavid deleted the api-v2-disable-structural-sharing branch December 23, 2025 16:14
imobachgs added a commit that referenced this pull request Jan 10, 2026
Merge the new HTTP API. Each PR has been already reviewed, so it should
be safe to merge it.

* #1829
* #2508
* #2772
* #2826
* #2848
* #2860
* #2863
* #2866
* #2867
* #2869
* #2870
* #2871
* #2872
* #2873
* #2874
* #2875
* #2876
* #2877
* #2880
* #2881
* #2882
* #2884
* #2885
* #2886
* #2891
* #2892
* #2893
* #2894
* #2895
* #2896
* #2897
* #2898
* #2899
* #2900
* #2901
* #2902
* #2903
* #2904
* #2908
* #2909
* #2910
* #2912
* #2913
* #2914
* #2915
* #2916
* #2917
* #2918
* #2920
* #2921
* #2923
* #2924
* #2926
* #2928
* #2929
* #2930
* #2933
* #2934
* #2935
* #2936
* #2938
* #2939
* #2942
* #2943
* #2944
* #2945
* #2946
* #2947
* #2948
* #2950
* #2951
* #2952
* #2954
* #2955
* #2956
* #2957
* #2958
* #2959
* #2960
* #2961
* #2963
* #2964
* #2965
* #2967
* #2968
* #2969
* #2970
* #2971
* #2972
* #2974
* #2975
* #2977
* #2978
* #2980
* #2982
* #2983
* #2984
* #2988
* #2989
* #2991
* #2992
* #2993
* #2994
* #2995
* #2996
* #2997
* #2999
@imobachgs imobachgs mentioned this pull request Mar 17, 2026
imobachgs added a commit that referenced this pull request Mar 17, 2026
Prepare to release version 19.

* #1829
* #2508
* #2772
* #2818
* #2826
* #2848
* #2860
* #2863
* #2864
* #2866
* #2867
* #2869
* #2870
* #2871
* #2872
* #2873
* #2874
* #2875
* #2876
* #2877
* #2880
* #2881
* #2882
* #2884
* #2885
* #2886
* #2891
* #2892
* #2893
* #2894
* #2895
* #2896
* #2897
* #2898
* #2899
* #2900
* #2901
* #2902
* #2903
* #2904
* #2908
* #2909
* #2910
* #2912
* #2913
* #2914
* #2915
* #2916
* #2917
* #2918
* #2920
* #2921
* #2923
* #2924
* #2926
* #2928
* #2929
* #2930
* #2933
* #2934
* #2935
* #2936
* #2937
* #2938
* #2939
* #2942
* #2943
* #2944
* #2945
* #2946
* #2947
* #2948
* #2949
* #2950
* #2951
* #2952
* #2954
* #2955
* #2956
* #2957
* #2958
* #2959
* #2960
* #2961
* #2963
* #2964
* #2965
* #2967
* #2968
* #2969
* #2970
* #2971
* #2972
* #2974
* #2975
* #2977
* #2978
* #2980
* #2981
* #2982
* #2983
* #2984
* #2988
* #2989
* #2990
* #2991
* #2992
* #2993
* #2994
* #2995
* #2996
* #2997
* #2998
* #2999
* #3000
* #3001
* #3002
* #3004
* #3005
* #3006
* #3007
* #3008
* #3009
* #3011
* #3012
* #3013
* #3014
* #3015
* #3016
* #3018
* #3019
* #3020
* #3021
* #3022
* #3023
* #3024
* #3025
* #3026
* #3027
* #3028
* #3029
* #3030
* #3031
* #3033
* #3034
* #3035
* #3036
* #3037
* #3039
* #3040
* #3041
* #3042
* #3043
* #3044
* #3045
* #3046
* #3047
* #3048
* #3049
* #3050
* #3051
* #3052
* #3053
* #3054
* #3055
* #3056
* #3057
* #3058
* #3060
* #3061
* #3062
* #3063
* #3064
* #3065
* #3066
* #3067
* #3068
* #3069
* #3070
* #3071
* #3072
* #3073
* #3074
* #3075
* #3076
* #3077
* #3078
* #3079
* #3086
* #3087
* #3088
* #3089
* #3090
* #3091
* #3092
* #3093
* #3094
* #3095
* #3096
* #3097
* #3098
* #3099
* #3100
* #3101
* #3102
* #3103
* #3104
* #3105
* #3106
* #3107
* #3108
* #3109
* #3110
* #3112
* #3113
* #3114
* #3115
* #3116
* #3117
* #3118
* #3119
* #3120
* #3122
* #3123
* #3124
* #3127
* #3128
* #3129
* #3130
* #3131
* #3133
* #3134
* #3135
* #3136
* #3137
* #3138
* #3139
* #3140
* #3141
* #3142
* #3143
* #3144
* #3145
* #3146
* #3147
* #3148
* #3149
* #3150
* #3151
* #3152
* #3153
* #3154
* #3155
* #3157
* #3158
* #3159
* #3160
* #3161
* #3162
* #3163
* #3164
* #3165
* #3166
* #3167
* #3168
* #3169
* #3170
* #3174
* #3175
* #3176
* #3177
* #3178
* #3179
* #3181
* #3182
* #3184
* #3185
* #3186
* #3188
* #3189
* #3190
* #3191
* #3192
* #3194
* #3195
* #3196
* #3197
* #3198
* #3199
* #3200
* #3201
* #3202
* #3203
* #3205
* #3206
* #3208
* #3209
* #3210
* #3213
* #3214
* #3215
* #3216
* #3217
* #3218
* #3219
* #3220
* #3222
* #3223
* #3224
* #3225
* #3226
* #3227
* #3228
* #3229
* #3230
* #3231
* #3232
* #3233
* #3234
* #3235
* #3236
* #3237
* #3238
* #3239
* #3240
* #3241
* #3242
* #3243
* #3244
* #3246
* #3247
* #3248
* #3250
* #3251
* #3252
* #3253
* #3254
* #3255
* #3256
* #3257
* #3258
* #3259
* #3260
* #3261
* #3262
* #3263
* #3265
* #3266
* #3267
* #3268
* #3269
* #3270
* #3271
* #3272
* #3273
* #3274
* #3275
* #3276
* #3277
* #3278
* #3279
* #3280
* #3281
* #3282
* #3283
* #3284
* #3285
* #3286
* #3287
* #3288
* #3289
* #3290
* #3291
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants