From 439bfcb2fcd8598c435912ae6533a9bf60bd549b Mon Sep 17 00:00:00 2001 From: E-Liang Tan Date: Mon, 7 Dec 2020 17:30:50 +0800 Subject: [PATCH 1/9] Improve venue search box responsiveness --- website/src/views/venues/VenuesContainer.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/website/src/views/venues/VenuesContainer.tsx b/website/src/views/venues/VenuesContainer.tsx index 27cb240b52..2b3cb2738c 100644 --- a/website/src/views/venues/VenuesContainer.tsx +++ b/website/src/views/venues/VenuesContainer.tsx @@ -1,4 +1,11 @@ -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { + FC, + unstable_useDeferredValue as useDeferredValue, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; import Loadable, { LoadingComponentProps } from 'react-loadable'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -53,7 +60,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { setSearchQuery, ] = useState(() => 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 deferredSearchQuery = useDeferredValue(searchQuery); const [isAvailabilityEnabled, setIsAvailabilityEnabled] = useState(() => { const params = qs.parse(location.search); return !!(params.time && params.day && params.duration); From bb8b6af8663c8743ae425318579529949f98c18e Mon Sep 17 00:00:00 2001 From: E-Liang Tan Date: Tue, 8 Dec 2020 20:52:46 +0800 Subject: [PATCH 2/9] useDeferredValue(isAvailabilityEnabled) with spinner to reduce page freezing --- website/src/views/components/SearchBox.tsx | 8 ++--- .../views/venues/AvailabilitySearch.test.tsx | 2 +- website/src/views/venues/VenuesContainer.scss | 11 +++++++ website/src/views/venues/VenuesContainer.tsx | 33 ++++++++++++------- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/website/src/views/components/SearchBox.tsx b/website/src/views/components/SearchBox.tsx index 76b1d14b11..a8b28d6948 100644 --- a/website/src/views/components/SearchBox.tsx +++ b/website/src/views/components/SearchBox.tsx @@ -16,18 +16,18 @@ import styles from './SearchBox.scss'; export type Props = { className?: string; throttle: number; - isLoading: boolean; + isLoading?: boolean; value: string | null; placeholder?: string; onChange: (value: string) => void; - onSearch: () => void; + onSearch?: () => void; onBlur?: () => void; }; const SearchBox: FC = ({ className, throttle, - isLoading, + isLoading = false, value, placeholder, onChange, @@ -55,7 +55,7 @@ const SearchBox: FC = ({ debounce( () => { isDirty.current = false; - onSearch(); + onSearch?.(); }, throttle, { leading: false }, diff --git a/website/src/views/venues/AvailabilitySearch.test.tsx b/website/src/views/venues/AvailabilitySearch.test.tsx index 2a4f3b401a..6257641d6f 100644 --- a/website/src/views/venues/AvailabilitySearch.test.tsx +++ b/website/src/views/venues/AvailabilitySearch.test.tsx @@ -1,6 +1,6 @@ import { defaultSearchOptions } from 'views/venues/AvailabilitySearch'; -describe('defaultSearchOptions', () => { +describe(defaultSearchOptions, () => { test('should the nearest slots during school hours', () => { // Monday expect(defaultSearchOptions(new Date('2018-01-15T12:30:00'))).toMatchObject({ diff --git a/website/src/views/venues/VenuesContainer.scss b/website/src/views/venues/VenuesContainer.scss index c49700392b..82d07a40c8 100644 --- a/website/src/views/venues/VenuesContainer.scss +++ b/website/src/views/venues/VenuesContainer.scss @@ -113,6 +113,17 @@ $venue-list-width: 16rem; border-radius: 0 0 $border-radius $border-radius; } +.availabilitySpinner { + display: inline-block; + width: 1.4rem; + height: 1.4rem; + margin: 0 0.3rem -0.1rem 0; + border-width: 0.2rem; + // Use parent button's text color so that the spinner is still visible when + // the button is hovered over. + border-left-color: initial; +} + .noVenueSelected { $icon-size: 6rem; diff --git a/website/src/views/venues/VenuesContainer.tsx b/website/src/views/venues/VenuesContainer.tsx index 2b3cb2738c..f98ba52d05 100644 --- a/website/src/views/venues/VenuesContainer.tsx +++ b/website/src/views/venues/VenuesContainer.tsx @@ -13,7 +13,7 @@ import { Location, locationsAreEqual } from 'history'; import classnames from 'classnames'; import axios from 'axios'; import qs from 'query-string'; -import { isEqual, mapValues, noop, pick, size } from 'lodash'; +import { isEqual, mapValues, pick, size } from 'lodash'; import type { TimePeriod, Venue, VenueDetailList, VenueSearchOptions } from 'types/venues'; import type { Subtract } from 'types/utils'; @@ -53,7 +53,6 @@ export const VenuesContainerComponent: FC = ({ venues }) => { const location = useLocation(); const matchParams = useParams(); - // Search state const [ /** Value of the controlled search box; updated real-time */ searchQuery, @@ -61,10 +60,13 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ] = useState(() => qs.parse(location.search).q || ''); /** Actual string to search with; deferred update */ const deferredSearchQuery = useDeferredValue(searchQuery); + const [isAvailabilityEnabled, setIsAvailabilityEnabled] = useState(() => { const params = qs.parse(location.search); return !!(params.time && params.day && params.duration); }); + const deferredIsAvailabilityEnabled = useDeferredValue(isAvailabilityEnabled); + const [searchOptions, setSearchOptions] = useState(() => { const params = qs.parse(location.search); // Extract searchOptions from the query string if they are present @@ -103,14 +105,19 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ); const highlightPeriod = useMemo(() => { - if (!isAvailabilityEnabled) return undefined; + if (!deferredIsAvailabilityEnabled) return undefined; return { day: searchOptions.day, startTime: convertIndexToTime(searchOptions.time * 2), endTime: convertIndexToTime(2 * (searchOptions.time + searchOptions.duration)), }; - }, [isAvailabilityEnabled, searchOptions.day, searchOptions.duration, searchOptions.time]); + }, [ + deferredIsAvailabilityEnabled, + searchOptions.day, + searchOptions.duration, + searchOptions.time, + ]); const selectedVenue = useMemo( () => (matchParams.venue ? decodeURIComponent(matchParams.venue) : undefined), @@ -121,7 +128,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { useEffect(() => { let query: Partial = {}; if (deferredSearchQuery) query.q = deferredSearchQuery; - if (isAvailabilityEnabled) query = { ...query, ...searchOptions }; + if (deferredIsAvailabilityEnabled) query = { ...query, ...searchOptions }; const search = qs.stringify(query); const pathname = venuePage(selectedVenue); @@ -139,17 +146,17 @@ export const VenuesContainerComponent: FC = ({ venues }) => { } }, [ debouncedHistory, + deferredIsAvailabilityEnabled, deferredSearchQuery, history, - isAvailabilityEnabled, searchOptions, selectedVenue, ]); const matchedVenues = useMemo(() => { const matched = searchVenue(venues, deferredSearchQuery); - return isAvailabilityEnabled ? filterAvailability(matched, searchOptions) : matched; - }, [isAvailabilityEnabled, searchOptions, deferredSearchQuery, venues]); + return deferredIsAvailabilityEnabled ? filterAvailability(matched, searchOptions) : matched; + }, [deferredIsAvailabilityEnabled, searchOptions, deferredSearchQuery, venues]); const matchedVenueNames = useMemo(() => matchedVenues.map(([venue]) => venue), [matchedVenues]); function renderSearch() { @@ -160,23 +167,25 @@ export const VenuesContainerComponent: FC = ({ venues }) => { {isAvailabilityEnabled && ( From a6d0cabc8331b648d27324b60af21edc55a5ba9b Mon Sep 17 00:00:00 2001 From: E-Liang Tan Date: Fri, 18 Dec 2020 17:59:12 +0800 Subject: [PATCH 3/9] Move spinner to an overlay --- website/src/styles/main.scss | 1 + website/src/styles/utils/deferred.scss | 11 +++ .../src/views/components/LoadingOverlay.scss | 9 +++ .../src/views/components/LoadingOverlay.tsx | 19 +++++ .../ModuleFinderContainer.scss | 10 --- .../ModuleFinderContainer.tsx | 13 ++-- website/src/views/venues/VenuesContainer.scss | 11 --- website/src/views/venues/VenuesContainer.tsx | 69 +++++++++++-------- 8 files changed, 87 insertions(+), 56 deletions(-) create mode 100644 website/src/styles/utils/deferred.scss create mode 100644 website/src/views/components/LoadingOverlay.scss create mode 100644 website/src/views/components/LoadingOverlay.tsx diff --git a/website/src/styles/main.scss b/website/src/styles/main.scss index de3700646f..b0b577fa0b 100644 --- a/website/src/styles/main.scss +++ b/website/src/styles/main.scss @@ -16,6 +16,7 @@ @import 'utils/workload'; @import 'utils/themes'; @import 'utils/scrollable'; +@import 'utils/deferred'; // Layout @import 'layout/site'; diff --git a/website/src/styles/utils/deferred.scss b/website/src/styles/utils/deferred.scss new file mode 100644 index 0000000000..c2699500f0 --- /dev/null +++ b/website/src/styles/utils/deferred.scss @@ -0,0 +1,11 @@ +// Source: https://reactjs.org/docs/concurrent-mode-patterns.html#delaying-a-pending-indicator +.deferred { + visibility: hidden; + animation: 0s linear 0.3s forwards makeVisible; + + @keyframes makeVisible { + to { + visibility: visible; + } + } +} diff --git a/website/src/views/components/LoadingOverlay.scss b/website/src/views/components/LoadingOverlay.scss new file mode 100644 index 0000000000..a5b9300caf --- /dev/null +++ b/website/src/views/components/LoadingOverlay.scss @@ -0,0 +1,9 @@ +.loadingOverlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 100; + background: var(--body-bg-70); +} diff --git a/website/src/views/components/LoadingOverlay.tsx b/website/src/views/components/LoadingOverlay.tsx new file mode 100644 index 0000000000..ae5b7c5504 --- /dev/null +++ b/website/src/views/components/LoadingOverlay.tsx @@ -0,0 +1,19 @@ +import classnames from 'classnames'; +import type { FC } from 'react'; +import styles from './LoadingOverlay.scss'; + +type Props = { + deferred?: boolean; +}; + +const LoadingOverlay: FC = ({ children, deferred }) => ( +
+ {children} +
+); + +export default LoadingOverlay; diff --git a/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.scss b/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.scss index ed2e5299fa..7b10bd4578 100644 --- a/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.scss +++ b/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.scss @@ -25,13 +25,3 @@ div.modulesPageContainer { text-align: right; color: var(--gray); } - -.loadingOverlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 100; - background: var(--body-bg-70); -} diff --git a/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.tsx b/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.tsx index 7cc9e5fa98..bb29355de7 100644 --- a/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.tsx +++ b/website/src/views/modules/ModuleFinderContainer/ModuleFinderContainer.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import type { FC } from 'react'; import { Hits, HitsStats, @@ -12,9 +12,10 @@ import { } from 'searchkit'; import classnames from 'classnames'; -import { ElasticSearchResult } from 'types/vendor/elastic-search'; -import { ModuleInformation } from 'types/modules'; +import type { ElasticSearchResult } from 'types/vendor/elastic-search'; +import type { ModuleInformation } from 'types/modules'; +import LoadingOverlay from 'views/components/LoadingOverlay'; import ModuleFinderSidebar from 'views/modules/ModuleFinderSidebar'; import ModuleSearchBox from 'views/modules/ModuleSearchBox'; import ModuleFinderNoHits from 'views/errors/ModuleFinderNoHits'; @@ -38,7 +39,7 @@ const searchkit = new SearchkitManager(esHostUrl, { const pageHead = Modules; -const ModuleInformationListComponent: React.FC = ({ hits }) => ( +const ModuleInformationListComponent: FC = ({ hits }) => (
    {hits.map((hit) => { const result = hit as ElasticSearchResult; @@ -55,7 +56,7 @@ const ModuleInformationListComponent: React.FC = ({ hits }) => (
); -const ModuleFinderContainer: React.FC = () => ( +const ModuleFinderContainer: FC = () => (
{pageHead} @@ -73,7 +74,7 @@ const ModuleFinderContainer: React.FC = () => ( /> -
+ diff --git a/website/src/views/venues/VenuesContainer.scss b/website/src/views/venues/VenuesContainer.scss index 82d07a40c8..c49700392b 100644 --- a/website/src/views/venues/VenuesContainer.scss +++ b/website/src/views/venues/VenuesContainer.scss @@ -113,17 +113,6 @@ $venue-list-width: 16rem; border-radius: 0 0 $border-radius $border-radius; } -.availabilitySpinner { - display: inline-block; - width: 1.4rem; - height: 1.4rem; - margin: 0 0.3rem -0.1rem 0; - border-width: 0.2rem; - // Use parent button's text color so that the spinner is still visible when - // the button is hovered over. - border-left-color: initial; -} - .noVenueSelected { $icon-size: 6rem; diff --git a/website/src/views/venues/VenuesContainer.tsx b/website/src/views/venues/VenuesContainer.tsx index f98ba52d05..b366e095c4 100644 --- a/website/src/views/venues/VenuesContainer.tsx +++ b/website/src/views/venues/VenuesContainer.tsx @@ -1,6 +1,6 @@ import { FC, - unstable_useDeferredValue as useDeferredValue, + unstable_useTransition as useTransition, useCallback, useEffect, useMemo, @@ -21,6 +21,7 @@ import type { Subtract } from 'types/utils'; import deferComponentRender from 'views/hocs/deferComponentRender'; import ApiError from 'views/errors/ApiError'; import Warning from 'views/errors/Warning'; +import LoadingOverlay from 'views/components/LoadingOverlay'; import LoadingSpinner from 'views/components/LoadingSpinner'; import SearchBox from 'views/components/SearchBox'; import { Clock } from 'react-feather'; @@ -53,19 +54,23 @@ export const VenuesContainerComponent: FC = ({ venues }) => { const location = useLocation(); const matchParams = useParams(); - const [ - /** Value of the controlled search box; updated real-time */ - searchQuery, - setSearchQuery, - ] = useState(() => qs.parse(location.search).q || ''); - /** Actual string to search with; deferred update */ - const deferredSearchQuery = useDeferredValue(searchQuery); + const [startTransition, isPending] = useTransition(); + + const [searchQuery, setSearchQuery] = useState(() => qs.parse(location.search).q || ''); + const [deferredSearchQuery, setDeferredSearchQuery] = useState(searchQuery); + + const handleSearchChange = useCallback((newSearchQuery: string) => { + setSearchQuery(newSearchQuery); + startTransition(() => setDeferredSearchQuery(newSearchQuery)); + }, []); const [isAvailabilityEnabled, setIsAvailabilityEnabled] = useState(() => { const params = qs.parse(location.search); return !!(params.time && params.day && params.duration); }); - const deferredIsAvailabilityEnabled = useDeferredValue(isAvailabilityEnabled); + const [deferredIsAvailabilityEnabled, setDeferredIsAvailabilityEnabled] = useState( + isAvailabilityEnabled, + ); const [searchOptions, setSearchOptions] = useState(() => { const params = qs.parse(location.search); @@ -84,14 +89,18 @@ export const VenuesContainerComponent: FC = ({ venues }) => { }, []); const onFindFreeRoomsClicked = useCallback(() => { - setIsAvailabilityEnabled(!isAvailabilityEnabled); - if (pristineSearchOptions && !isAvailabilityEnabled) { - // Only reset search options if the user has never changed it, and if the - // search box is being opened. By resetting the option when the box is opened, - // the time when the box is opened will be used, instead of the time when the - // page is loaded - setSearchOptions(defaultSearchOptions()); - } + const newIsAvailabilityEnabled = !isAvailabilityEnabled; + setIsAvailabilityEnabled(newIsAvailabilityEnabled); + startTransition(() => { + setDeferredIsAvailabilityEnabled(newIsAvailabilityEnabled); + if (pristineSearchOptions && newIsAvailabilityEnabled) { + // Only reset search options if the user has never changed it, and if the + // search box is being opened. By resetting the option when the box is opened, + // the time when the box is opened will be used, instead of the time when the + // page is loaded + setSearchOptions(defaultSearchOptions()); + } + }); }, [isAvailabilityEnabled, pristineSearchOptions]); const onAvailabilityUpdate = useCallback( @@ -169,7 +178,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { throttle={0} value={searchQuery} placeholder="e.g. LT27" - onChange={setSearchQuery} + onChange={handleSearchChange} /> {isAvailabilityEnabled && ( @@ -201,7 +205,10 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ); } - const disableAvailability = useCallback(() => setIsAvailabilityEnabled(false), []); + const disableAvailability = useCallback(() => { + setIsAvailabilityEnabled(false); + startTransition(() => setDeferredIsAvailabilityEnabled(false)); + }, []); function renderNoResult() { const unfilteredCount = size(matchedVenues); return ( @@ -229,11 +236,15 @@ export const VenuesContainerComponent: FC = ({ venues }) => {
{renderSearch()} - {size(matchedVenues) === 0 ? ( - renderNoResult() - ) : ( +
+ {isPending && ( + + + + )} + {!isPending && size(matchedVenues) === 0 && renderNoResult()} - )} +
Date: Fri, 18 Dec 2020 18:13:28 +0800 Subject: [PATCH 4/9] Reduce defer delay to improve perceived responsiveness --- website/src/styles/utils/deferred.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/styles/utils/deferred.scss b/website/src/styles/utils/deferred.scss index c2699500f0..15eb009a24 100644 --- a/website/src/styles/utils/deferred.scss +++ b/website/src/styles/utils/deferred.scss @@ -1,7 +1,7 @@ // Source: https://reactjs.org/docs/concurrent-mode-patterns.html#delaying-a-pending-indicator .deferred { visibility: hidden; - animation: 0s linear 0.3s forwards makeVisible; + animation: 0s linear 0.2s forwards makeVisible; @keyframes makeVisible { to { From 4423a00f0155eb4dd2345a26a8b97257ea75e52a Mon Sep 17 00:00:00 2001 From: E-Liang Tan Date: Fri, 18 Dec 2020 18:13:49 +0800 Subject: [PATCH 5/9] Defer searchOptions --- website/src/views/venues/VenuesContainer.tsx | 48 +++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/website/src/views/venues/VenuesContainer.tsx b/website/src/views/venues/VenuesContainer.tsx index b366e095c4..3b463683e6 100644 --- a/website/src/views/venues/VenuesContainer.tsx +++ b/website/src/views/venues/VenuesContainer.tsx @@ -81,6 +81,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ) as VenueSearchOptions) : defaultSearchOptions(); }); + const [deferredSearchOptions, setDeferredSearchOptions] = useState(searchOptions); const [pristineSearchOptions, setPristineSearchOptions] = useState(() => !isAvailabilityEnabled); // TODO: Check if this actually does anything useful @@ -90,15 +91,22 @@ export const VenuesContainerComponent: FC = ({ venues }) => { const onFindFreeRoomsClicked = useCallback(() => { const newIsAvailabilityEnabled = !isAvailabilityEnabled; + + // Only reset search options if the user has never changed it, and if the + // search box is being opened. By resetting the option when the box is + // opened, the time when the box is opened will be used, instead of the time + // when the page is loaded. + const shouldResetSearchOptions = pristineSearchOptions && newIsAvailabilityEnabled; + setIsAvailabilityEnabled(newIsAvailabilityEnabled); + if (shouldResetSearchOptions) { + setSearchOptions(defaultSearchOptions()); + } + startTransition(() => { setDeferredIsAvailabilityEnabled(newIsAvailabilityEnabled); - if (pristineSearchOptions && newIsAvailabilityEnabled) { - // Only reset search options if the user has never changed it, and if the - // search box is being opened. By resetting the option when the box is opened, - // the time when the box is opened will be used, instead of the time when the - // page is loaded - setSearchOptions(defaultSearchOptions()); + if (shouldResetSearchOptions) { + setDeferredSearchOptions(defaultSearchOptions()); } }); }, [isAvailabilityEnabled, pristineSearchOptions]); @@ -106,8 +114,10 @@ export const VenuesContainerComponent: FC = ({ venues }) => { const onAvailabilityUpdate = useCallback( (newSearchOptions: VenueSearchOptions) => { if (!isEqual(newSearchOptions, searchOptions)) { - setSearchOptions(clampClassDuration(newSearchOptions)); + const clampedSearchOptions = clampClassDuration(newSearchOptions); + setSearchOptions(clampedSearchOptions); setPristineSearchOptions(false); // user changed searchOptions + startTransition(() => setDeferredSearchOptions(clampedSearchOptions)); } }, [searchOptions], @@ -117,15 +127,17 @@ export const VenuesContainerComponent: FC = ({ venues }) => { if (!deferredIsAvailabilityEnabled) return undefined; return { - day: searchOptions.day, - startTime: convertIndexToTime(searchOptions.time * 2), - endTime: convertIndexToTime(2 * (searchOptions.time + searchOptions.duration)), + day: deferredSearchOptions.day, + startTime: convertIndexToTime(deferredSearchOptions.time * 2), + endTime: convertIndexToTime( + 2 * (deferredSearchOptions.time + deferredSearchOptions.duration), + ), }; }, [ deferredIsAvailabilityEnabled, - searchOptions.day, - searchOptions.duration, - searchOptions.time, + deferredSearchOptions.day, + deferredSearchOptions.duration, + deferredSearchOptions.time, ]); const selectedVenue = useMemo( @@ -137,7 +149,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { useEffect(() => { let query: Partial = {}; if (deferredSearchQuery) query.q = deferredSearchQuery; - if (deferredIsAvailabilityEnabled) query = { ...query, ...searchOptions }; + if (deferredIsAvailabilityEnabled) query = { ...query, ...deferredSearchOptions }; const search = qs.stringify(query); const pathname = venuePage(selectedVenue); @@ -156,16 +168,18 @@ export const VenuesContainerComponent: FC = ({ venues }) => { }, [ debouncedHistory, deferredIsAvailabilityEnabled, + deferredSearchOptions, deferredSearchQuery, history, - searchOptions, selectedVenue, ]); const matchedVenues = useMemo(() => { const matched = searchVenue(venues, deferredSearchQuery); - return deferredIsAvailabilityEnabled ? filterAvailability(matched, searchOptions) : matched; - }, [deferredIsAvailabilityEnabled, searchOptions, deferredSearchQuery, venues]); + return deferredIsAvailabilityEnabled + ? filterAvailability(matched, deferredSearchOptions) + : matched; + }, [deferredIsAvailabilityEnabled, deferredSearchOptions, deferredSearchQuery, venues]); const matchedVenueNames = useMemo(() => matchedVenues.map(([venue]) => venue), [matchedVenues]); function renderSearch() { From ce167bc94b04381e56a354367afd478dfd3a3656 Mon Sep 17 00:00:00 2001 From: E-Liang Tan Date: Fri, 18 Dec 2020 20:49:17 +0800 Subject: [PATCH 6/9] Switch back to useDeferredValue as it's too messy to manage both primary and deferred values manually --- website/src/views/venues/VenuesContainer.tsx | 51 +++++++------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/website/src/views/venues/VenuesContainer.tsx b/website/src/views/venues/VenuesContainer.tsx index 3b463683e6..b9101ddf5d 100644 --- a/website/src/views/venues/VenuesContainer.tsx +++ b/website/src/views/venues/VenuesContainer.tsx @@ -1,6 +1,6 @@ import { FC, - unstable_useTransition as useTransition, + unstable_useDeferredValue as useDeferredValue, useCallback, useEffect, useMemo, @@ -54,23 +54,15 @@ export const VenuesContainerComponent: FC = ({ venues }) => { const location = useLocation(); const matchParams = useParams(); - const [startTransition, isPending] = useTransition(); - const [searchQuery, setSearchQuery] = useState(() => qs.parse(location.search).q || ''); - const [deferredSearchQuery, setDeferredSearchQuery] = useState(searchQuery); - const handleSearchChange = useCallback((newSearchQuery: string) => { setSearchQuery(newSearchQuery); - startTransition(() => setDeferredSearchQuery(newSearchQuery)); }, []); const [isAvailabilityEnabled, setIsAvailabilityEnabled] = useState(() => { const params = qs.parse(location.search); return !!(params.time && params.day && params.duration); }); - const [deferredIsAvailabilityEnabled, setDeferredIsAvailabilityEnabled] = useState( - isAvailabilityEnabled, - ); const [searchOptions, setSearchOptions] = useState(() => { const params = qs.parse(location.search); @@ -81,43 +73,37 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ) as VenueSearchOptions) : defaultSearchOptions(); }); - const [deferredSearchOptions, setDeferredSearchOptions] = useState(searchOptions); const [pristineSearchOptions, setPristineSearchOptions] = useState(() => !isAvailabilityEnabled); + const deferredSearchQuery = useDeferredValue(searchQuery); + const deferredIsAvailabilityEnabled = useDeferredValue(isAvailabilityEnabled); + const deferredSearchOptions = useDeferredValue(searchOptions); + const isPending = + searchQuery !== deferredSearchQuery || + isAvailabilityEnabled !== deferredIsAvailabilityEnabled || + searchOptions !== deferredSearchOptions; + // TODO: Check if this actually does anything useful useEffect(() => { VenueLocation.preload(); }, []); const onFindFreeRoomsClicked = useCallback(() => { - const newIsAvailabilityEnabled = !isAvailabilityEnabled; - - // Only reset search options if the user has never changed it, and if the - // search box is being opened. By resetting the option when the box is - // opened, the time when the box is opened will be used, instead of the time - // when the page is loaded. - const shouldResetSearchOptions = pristineSearchOptions && newIsAvailabilityEnabled; - - setIsAvailabilityEnabled(newIsAvailabilityEnabled); - if (shouldResetSearchOptions) { + setIsAvailabilityEnabled(!isAvailabilityEnabled); + if (pristineSearchOptions && !isAvailabilityEnabled) { + // Only reset search options if the user has never changed it, and if the + // search box is being opened. By resetting the option when the box is + // opened, the time when the box is opened will be used, instead of the + // time when the page is loaded. setSearchOptions(defaultSearchOptions()); } - - startTransition(() => { - setDeferredIsAvailabilityEnabled(newIsAvailabilityEnabled); - if (shouldResetSearchOptions) { - setDeferredSearchOptions(defaultSearchOptions()); - } - }); }, [isAvailabilityEnabled, pristineSearchOptions]); const onAvailabilityUpdate = useCallback( (newSearchOptions: VenueSearchOptions) => { if (!isEqual(newSearchOptions, searchOptions)) { - const clampedSearchOptions = clampClassDuration(newSearchOptions); - setSearchOptions(clampedSearchOptions); + setSearchOptions(clampClassDuration(newSearchOptions)); setPristineSearchOptions(false); // user changed searchOptions - startTransition(() => setDeferredSearchOptions(clampedSearchOptions)); } }, [searchOptions], @@ -219,10 +205,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { ); } - const disableAvailability = useCallback(() => { - setIsAvailabilityEnabled(false); - startTransition(() => setDeferredIsAvailabilityEnabled(false)); - }, []); + const disableAvailability = useCallback(() => setIsAvailabilityEnabled(false), []); function renderNoResult() { const unfilteredCount = size(matchedVenues); return ( From 391e96bb2c56f7dba59b935fd63d64d04ea40766 Mon Sep 17 00:00:00 2001 From: E-Liang Tan Date: Sun, 20 Dec 2020 14:37:27 +0800 Subject: [PATCH 7/9] Fix review comment: add `onSearch` and `onChange` docstrings --- website/src/views/components/SearchBox.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/src/views/components/SearchBox.tsx b/website/src/views/components/SearchBox.tsx index a8b28d6948..edcb91a205 100644 --- a/website/src/views/components/SearchBox.tsx +++ b/website/src/views/components/SearchBox.tsx @@ -19,7 +19,9 @@ export type Props = { isLoading?: boolean; value: string | null; placeholder?: string; + /** Called when the search box value changes */ onChange: (value: string) => void; + /** Called when a search should be triggered, potentially debounced by `throttle` milliseconds. */ onSearch?: () => void; onBlur?: () => void; }; From e596bc534adc326ae5ad0afd279b2d798787e0ac Mon Sep 17 00:00:00 2001 From: E-Liang Tan Date: Sun, 20 Dec 2020 14:41:48 +0800 Subject: [PATCH 8/9] Fix review comment: remove unnecessary `handleSearchChange` --- website/src/views/venues/VenuesContainer.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/website/src/views/venues/VenuesContainer.tsx b/website/src/views/venues/VenuesContainer.tsx index b9101ddf5d..6d829c4021 100644 --- a/website/src/views/venues/VenuesContainer.tsx +++ b/website/src/views/venues/VenuesContainer.tsx @@ -55,9 +55,6 @@ export const VenuesContainerComponent: FC = ({ venues }) => { const matchParams = useParams(); const [searchQuery, setSearchQuery] = useState(() => qs.parse(location.search).q || ''); - const handleSearchChange = useCallback((newSearchQuery: string) => { - setSearchQuery(newSearchQuery); - }, []); const [isAvailabilityEnabled, setIsAvailabilityEnabled] = useState(() => { const params = qs.parse(location.search); @@ -178,7 +175,7 @@ export const VenuesContainerComponent: FC = ({ venues }) => { throttle={0} value={searchQuery} placeholder="e.g. LT27" - onChange={handleSearchChange} + onChange={setSearchQuery} />