Optimize Venues search using React Concurrent Mode APIs#3040
Conversation
Codecov Report
@@ Coverage Diff @@
## eliang/concurrent-mode #3040 +/- ##
==========================================================
+ Coverage 57.16% 57.25% +0.08%
==========================================================
Files 257 258 +1
Lines 5286 5297 +11
Branches 1206 1212 +6
==========================================================
+ Hits 3022 3033 +11
Misses 2264 2264
Continue to review full report at Codecov.
|
|
Deployment preview for |
| {isAvailabilityEnabled !== deferredIsAvailabilityEnabled ? ( | ||
| <LoadingSpinner className={styles.availabilitySpinner} white={isAvailabilityEnabled} /> |
There was a problem hiding this comment.
This causes the spinner to flash on faster browsers. No idea how to fix it, but I think storing isAvailabilityEnabled !== deferredIsAvailabilityEnabled in a useState and using our defer may be able to fix it at the cost of more renders?
There was a problem hiding this comment.
Oh, just click the Find free rooms button. The clock icon in the button will disappear for a moment as the spinner flashes
There was a problem hiding this comment.
I'm not sure if putting the spinner here is the best idea - the thing that's "loading" is the venue list below, and it's quite easily missed (though the animation helps). Won't putting a spinner there instead work?
Also thinking about https://reactjs.org/docs/concurrent-mode-patterns.html#delaying-a-pending-indicator, but I don't know how to make that work with useDeferredValue
There was a problem hiding this comment.
Ooh, deferring the loading indicator in CSS is brilliant. I'll switch us to useTransition then, especially since the current condition for determining whether to show the indicator or not is a bit of a hack.
There was a problem hiding this comment.
I lied, useTransition isn't a good fit for our use case. It's back to useDeferredValue, but with a deferred spinner :D
| <button | ||
| className={classnames( | ||
| 'btn btn-block btn-svg', | ||
| styles.availabilityToggle, |
There was a problem hiding this comment.
This style didn't exist
4506e67 to
156c1e3
Compare
c8201a6 to
07601ec
Compare
|
Rebased on parent branch, and fixed conflict with |
156c1e3 to
de2dee6
Compare
…ary and deferred values manually
26d33b3 to
ce167bc
Compare
|
Rebased on parent branch, no diff change |
| // Source: https://reactjs.org/docs/concurrent-mode-patterns.html#delaying-a-pending-indicator | ||
| .deferred { | ||
| visibility: hidden; | ||
| animation: 0s linear 0.2s forwards makeVisible; |
There was a problem hiding this comment.
We could in theory make this configurable from the component side using CSS variables or even inline styles, but probably YAGNI
There was a problem hiding this comment.
Yeah, probably not a good idea to overengineer this right now hahaa
| placeholder?: string; | ||
| onChange: (value: string) => void; | ||
| onSearch: () => void; | ||
| onSearch?: () => void; |
There was a problem hiding this comment.
Might be useful adding a small comment on the difference between this and onChange
| /> | ||
|
|
||
| <LoadingComponent> | ||
| <div className={styles.loadingOverlay} /> |
There was a problem hiding this comment.
Should remove the unused duplicate class here from ModuleFinderContainer.scss
There was a problem hiding this comment.
Yep, it was already removed
| ] = useState<string>(() => qs.parse(location.search).q || ''); | ||
| /** Actual string to search with; deferred update */ | ||
| const deferredSearchQuery = searchQuery; // TODO: Redundant now. Use React.useDeferredValue after we adopt concurrent mode | ||
| const [searchQuery, setSearchQuery] = useState<string>(() => qs.parse(location.search).q || ''); |
There was a problem hiding this comment.
/nit
| const [searchQuery, setSearchQuery] = useState<string>(() => qs.parse(location.search).q || ''); | |
| const [searchQuery, setSearchQuery] = useState(() => qs.parse(location.search).q || ''); |
There was a problem hiding this comment.
That doesn't work because qs.parse returns any. We could do the below type assertion instead, but the current code seems good enough
| const [searchQuery, setSearchQuery] = useState<string>(() => qs.parse(location.search).q || ''); | |
| const [searchQuery, setSearchQuery] = useState( | |
| () => (qs.parse(location.search) as Params).q || '', | |
| ); |
| const handleSearchChange = useCallback((newSearchQuery: string) => { | ||
| setSearchQuery(newSearchQuery); | ||
| }, []); |
There was a problem hiding this comment.
Isn't handleSearchChange exactly the same as setSearchQuery?
There was a problem hiding this comment.
Oh yes, I think I forgot to change this back after my brief experiment with useTransition
| {size(matchedVenues) === 0 ? ( | ||
| renderNoResult() | ||
| ) : ( | ||
| <div className="position-relative"> |
There was a problem hiding this comment.
I think without specifying height this will not take up the whole height of the venues list right?
|
@ZhangYiJiang I think reading The TL;DR it looks like it's just storing the previous state in a |




TL;DR
Uses React Concurrent Mode APIs (added in #3039) to optimize the Venues components reworked in #3038.
Optimizations in this PR
Uses
useDeferredValueto allow the search box to be immediately updated in a sync render before a second, longer concurrent render is performed.masterThis may sound similar to the DIY deferred update using 2 nested
requestAnimationFramecalls that we currently have onmaster. However, that is inferior to this solution because:master, the app noticeably freezes up once a legacy render starts as it cannot be interrupted. In the gifs above, notice that onmaster, a bunch of characters appear at once -- this happens because I typed while the app was frozen.This is what typing a single character looks like in the React Scheduling Profiler (that I helped create :D):
TL;DR of how to read the React data (the stuff above the grey bar):
Notice the short sync render on the left (which is in response to the key press), followed by a very long render caused by the deferred value updating. Also notice that
VenueListtakes a long time to render.This is what typing a second character during a render looks like:
Notice that a concurrent render got abandoned.
Uses

useDeferredValueto allow the availability filter to open instantly. Onmaster, this just freezes as there were no optimizations for this.Note that we do not also defer
searchOptions, as I couldn't get it to feel right. It seems better to just let the page freeze while the render happens. (Edit: it's now deferred. See GIFs in this comment below: #3040 (comment))