Commit e475a82
Fix
This PR fixes an issue where you could run into a `Maximum update depth
exceeded` error in the `Listbox`, `Menu` or `Combobox` component in
combination with the `transition` prop.
It seems like some people ran into this, but I was never able to
reproduce it. But now I figured out what the issue is, how to reproduce
it and this PR should fix it as well (it at least fixes the `Maximum
update depth exceeded` I could reproduce).
To reproduce it, you don't need a lot of code, but you do need a
particular setup:
```tsx
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
let options = ['apple', 'banana', 'orange', 'grape', 'pear']
export default function App() {
let [value, setValue] = useState('apple')
return (
<>
{/* Required to reproduce the issue */}
<div className="min-h-dvh" />
<Listbox value={value} onChange={setValue}>
<ListboxButton>{value}</ListboxButton>
<ListboxOptions transition>
{options.map((option) => (
<ListboxOption key={option} value={option}>
{option}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</>
)
}
```
This setup means that you have to scroll down to see the Listbox. When
you open the Listbox, the `ListboxOptions` will be rendered increasing
the height of the page. If you then close it again, the page will
decrease in height again, causing the issue to happen.
The next question is, why does the crash happen?
The crash happens because we are updating some internal state based on
whether the `ListboxButton` moved or not. The reason we track whether
the button moved or not is to improve the UX (a bit ironic I know,
because it could crash…)
In the Listbox component (and some others like Menu and Combobox), we
track whether a button moved on the screen when you close the dropdown.
If it does move, we will immediately hide the Listbox without any
transitions.
This is done to improve the UX because we don't want to see a fading-out
dropdown somewhere else on the screen.
Maybe it's easier if I show you an example. This example is using our UI
Kit, Catalyst: https://catalyst.tailwindui.com/docs/listbox
**Example 1:**
1. Open the dropdown
2. Click outside or press Escape to close it
3. Notice the subtle fade-out transition
https://github.com/user-attachments/assets/0eecffd7-f4fe-4543-8a48-433dd27f1bf7
**Example 2:**
1. Open the dropdown
2. Press tab to focus the next element on the page
3. Notice the subtle fade-out transition
https://github.com/user-attachments/assets/3bec2178-1f15-40cd-ab1e-ae8ca333bd92
**Example 3:**
Now we're going to do something very similar, but this time the next
focusable element will be just outside of what is visual to the user.
This will cause the browser to scroll and scroll the new element into
view. We'll make the page smaller by opening devtools.
1. Open the dropdown
2. Press tab to focus the next element on the page
3. Notice how the the dropdown is completely gone, no transitions
whatsoever
https://github.com/user-attachments/assets/4abd7fd4-250b-4f91-a172-5954e9fccae7
**Example 4:**
Up until now, all these make sense as a user. But here is an example of
what would happen if we didn't close the dropdown immediately when the
button moved.
Same steps and similar setup as Example 3, but without the special code
that detects button moves.
It's going to be quick, but look near the end of this video. Once I
press tab, you will see the dropdown fade out and linger around near the
top of the screen.
https://github.com/user-attachments/assets/993fdec5-6a8c-468f-bbf8-6003f7be7048
(This was a little tricky to reproduce here because when you tab, the
active element will be scrolled into view automatically by the browser,
so the button sits in the middle of the page. But we want to make sure
the dropdown button is still in the viewport~ish. This is why it's
zoomed out a bit.)
---
Now that we established that, why did it crash in some scenarios?
The first important part is the `transition` prop. A transition allows
us to keep the dropdown visible even though its internal state is
"closed". This is important so that we will wait until all CSS
transitions are done before actually unmounting the component.
The next important part is about the extra div (`<div class="min-h-dvh"
/>`) in the reproduction code, because it will force a scrollbar.
Opening and closing the listbox will change the height of the page. This
in turn will move the button up to make room for the dropdown part,
therefore the `button` will be marked as moved.
However, we keep checking whether the button moved compared to the
initial position, which could result in a loop:
0. Close the listbox.
1. Compute button position at **position A**
2. Re-render happens, and button is now at **position B** because the
dropdown part is gone
3. Re-render happens, and button is now at **position A** again, because
it's considered at the same initial position so instead of instantly
hiding we actually show it again because the `visible` state (thanks to
the `transition`) is still set to true.
4. Re-render happens, and button is now at **position B** again because
now the dropdown is rendered again
5. …**position A**
6. …**position B**
7. …
To actually solve the issue, we have to make sure that once the button
is marked as moved, it will stay marked as "moved" until the `enabled`
boolean (argument to the hook) changes again.
I also did some internal refactors by moving this logic into the
existing state machines instead of relying on React and when things
render. I therefore also got rid of the `useDidElementMove` hook
entirely.
## Test plan
2. Error does not reproduce with this code.
2. The expected behavior in Catalyst still works as expected.
Simplified reproduction of the crash in action:
### Before
https://github.com/user-attachments/assets/46254740-dac6-4d95-ba1a-aef4b4b7082c
### After
https://github.com/user-attachments/assets/9ee4350b-3bd0-4f07-b4a5-edb4f3cb3d78
Fixes: #3655
Fixes: #3562
---------
Co-authored-by: Jordan Pittman <[email protected]>Maximum update depth exceeded due to Transition (#3782)1 parent 46ee372 commit e475a82
File tree
11 files changed
+238
-64
lines changed- packages/@headlessui-react
- src
- components
- combobox
- listbox
- hooks
- utils
11 files changed
+238
-64
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
Lines changed: 50 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
5 | 10 | | |
6 | 11 | | |
7 | 12 | | |
| |||
74 | 79 | | |
75 | 80 | | |
76 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
77 | 85 | | |
78 | 86 | | |
79 | 87 | | |
| |||
96 | 104 | | |
97 | 105 | | |
98 | 106 | | |
| 107 | + | |
| 108 | + | |
99 | 109 | | |
100 | 110 | | |
101 | 111 | | |
| |||
160 | 170 | | |
161 | 171 | | |
162 | 172 | | |
| 173 | + | |
163 | 174 | | |
164 | 175 | | |
165 | 176 | | |
166 | 177 | | |
167 | 178 | | |
168 | 179 | | |
169 | 180 | | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
170 | 184 | | |
171 | 185 | | |
172 | 186 | | |
| |||
181 | 195 | | |
182 | 196 | | |
183 | 197 | | |
| 198 | + | |
| 199 | + | |
184 | 200 | | |
185 | 201 | | |
186 | 202 | | |
| |||
197 | 213 | | |
198 | 214 | | |
199 | 215 | | |
| 216 | + | |
200 | 217 | | |
201 | 218 | | |
202 | 219 | | |
203 | 220 | | |
204 | | - | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
205 | 227 | | |
206 | 228 | | |
207 | 229 | | |
| |||
404 | 426 | | |
405 | 427 | | |
406 | 428 | | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
407 | 437 | | |
408 | 438 | | |
409 | 439 | | |
| |||
438 | 468 | | |
439 | 469 | | |
440 | 470 | | |
| 471 | + | |
441 | 472 | | |
442 | 473 | | |
443 | 474 | | |
| |||
464 | 495 | | |
465 | 496 | | |
466 | 497 | | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
467 | 512 | | |
468 | 513 | | |
469 | 514 | | |
| |||
640 | 685 | | |
641 | 686 | | |
642 | 687 | | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
643 | 692 | | |
644 | 693 | | |
645 | 694 | | |
| |||
Lines changed: 16 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1261 | 1261 | | |
1262 | 1262 | | |
1263 | 1263 | | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
1264 | 1279 | | |
1265 | 1280 | | |
1266 | 1281 | | |
| |||
1392 | 1407 | | |
1393 | 1408 | | |
1394 | 1409 | | |
1395 | | - | |
| 1410 | + | |
1396 | 1411 | | |
1397 | 1412 | | |
1398 | 1413 | | |
| |||
Lines changed: 44 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
4 | 9 | | |
5 | 10 | | |
6 | 11 | | |
| |||
65 | 70 | | |
66 | 71 | | |
67 | 72 | | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
68 | 76 | | |
69 | 77 | | |
70 | 78 | | |
| |||
82 | 90 | | |
83 | 91 | | |
84 | 92 | | |
| 93 | + | |
| 94 | + | |
85 | 95 | | |
86 | 96 | | |
87 | 97 | | |
| |||
135 | 145 | | |
136 | 146 | | |
137 | 147 | | |
| 148 | + | |
138 | 149 | | |
139 | 150 | | |
140 | 151 | | |
141 | 152 | | |
142 | 153 | | |
143 | 154 | | |
144 | 155 | | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
145 | 160 | | |
146 | 161 | | |
147 | 162 | | |
148 | 163 | | |
149 | 164 | | |
150 | 165 | | |
| 166 | + | |
151 | 167 | | |
152 | 168 | | |
153 | 169 | | |
| |||
169 | 185 | | |
170 | 186 | | |
171 | 187 | | |
| 188 | + | |
172 | 189 | | |
173 | 190 | | |
174 | 191 | | |
| |||
394 | 411 | | |
395 | 412 | | |
396 | 413 | | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
397 | 422 | | |
398 | 423 | | |
399 | 424 | | |
| |||
412 | 437 | | |
413 | 438 | | |
414 | 439 | | |
| 440 | + | |
415 | 441 | | |
416 | 442 | | |
417 | 443 | | |
| |||
447 | 473 | | |
448 | 474 | | |
449 | 475 | | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
450 | 490 | | |
451 | 491 | | |
452 | 492 | | |
| |||
566 | 606 | | |
567 | 607 | | |
568 | 608 | | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
569 | 613 | | |
570 | 614 | | |
571 | 615 | | |
| |||
Lines changed: 12 additions & 14 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
28 | 27 | | |
29 | 28 | | |
30 | 29 | | |
| |||
610 | 609 | | |
611 | 610 | | |
612 | 611 | | |
613 | | - | |
614 | | - | |
615 | | - | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
616 | 616 | | |
617 | | - | |
618 | | - | |
619 | | - | |
620 | | - | |
621 | | - | |
622 | | - | |
623 | | - | |
624 | | - | |
625 | | - | |
626 | | - | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
627 | 625 | | |
628 | 626 | | |
629 | 627 | | |
| |||
0 commit comments