@@ -143,12 +143,17 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
143
143
// eslint-disable-next-line react-hooks/exhaustive-deps
144
144
} , [ ] ) ;
145
145
146
+ let isUpdatingSize = useRef ( false ) ;
146
147
let updateSize = useEffectEvent ( ( flush : typeof flushSync ) => {
147
148
let dom = ref . current ;
148
- if ( ! dom ) {
149
+ if ( ! dom && ! isUpdatingSize . current ) {
149
150
return ;
150
151
}
151
152
153
+ // Prevent reentrancy when resize observer fires, triggers re-layout that results in
154
+ // content size update, causing below layout effect to fire. This avoids infinite loops.
155
+ isUpdatingSize . current = true ;
156
+
152
157
let isTestEnv = process . env . NODE_ENV === 'test' && ! process . env . VIRT_ON ;
153
158
let isClientWidthMocked = Object . getOwnPropertyNames ( window . HTMLElement . prototype ) . includes ( 'clientWidth' ) ;
154
159
let isClientHeightMocked = Object . getOwnPropertyNames ( window . HTMLElement . prototype ) . includes ( 'clientHeight' ) ;
@@ -177,27 +182,38 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
177
182
} ) ;
178
183
}
179
184
}
185
+
186
+ isUpdatingSize . current = false ;
180
187
} ) ;
181
188
182
- let didUpdateSize = useRef ( false ) ;
189
+ // Update visible rect when the content size changes, in case scrollbars need to appear or disappear.
190
+ let lastContentSize = useRef < Size | null > ( null ) ;
183
191
useLayoutEffect ( ( ) => {
184
- // React doesn't allow flushSync inside effects, so queue a microtask.
185
- // We also need to wait until all refs are set (e.g. when passing a ref down from a parent).
186
- queueMicrotask ( ( ) => {
187
- if ( ! didUpdateSize . current ) {
188
- didUpdateSize . current = true ;
189
- updateSize ( flushSync ) ;
192
+ if ( ! isUpdatingSize . current ) {
193
+ // Detect when scrollbar state might have changed.
194
+ let prevShowX = lastContentSize . current ?. width > state . width ;
195
+ let prevShowY = lastContentSize . current ?. height > state . height ;
196
+ let curShowX = contentSize . width > state . width ;
197
+ let curShowY = contentSize . height > state . height ;
198
+
199
+ if ( lastContentSize . current && prevShowX === curShowX && prevShowY === curShowY ) {
200
+ return ;
201
+ }
202
+
203
+ // React doesn't allow flushSync inside effects, so queue a microtask.
204
+ // We also need to wait until all refs are set (e.g. when passing a ref down from a parent).
205
+ // If we are in an `act` environment, update immediately without a microtask so you don't need
206
+ // to mock timers in tests. In this case, the update is synchronous already.
207
+ if ( globalThis . IS_REACT_ACT_ENVIRONMENT ) {
208
+ updateSize ( fn => fn ( ) ) ;
209
+ } else {
210
+ queueMicrotask ( ( ) => updateSize ( flushSync ) ) ;
190
211
}
191
- } ) ;
192
- } , [ updateSize ] ) ;
193
- useEffect ( ( ) => {
194
- if ( ! didUpdateSize . current ) {
195
- // If useEffect ran before the above microtask, we are in a synchronous render (e.g. act).
196
- // Update the size here so that you don't need to mock timers in tests.
197
- didUpdateSize . current = true ;
198
- updateSize ( fn => fn ( ) ) ;
199
212
}
200
- } , [ updateSize ] ) ;
213
+
214
+ lastContentSize . current = contentSize ;
215
+ } ) ;
216
+
201
217
let onResize = useCallback ( ( ) => {
202
218
updateSize ( flushSync ) ;
203
219
} , [ updateSize ] ) ;
0 commit comments