Skip to content

Commit d6cb312

Browse files
authored
Merge pull request #46 from thomasKn/sort
Sort and filters are now optimistic
2 parents 44f0b2d + 7b42ecc commit d6cb312

File tree

4 files changed

+275
-104
lines changed

4 files changed

+275
-104
lines changed

Diff for: app/components/collection/Filter.tsx

+73-35
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ import {
99
PrefetchPageLinks,
1010
useLocation,
1111
useNavigate,
12-
useNavigation,
1312
useSearchParams,
1413
} from '@remix-run/react';
1514
import {useCallback, useMemo, useState} from 'react';
1615
import useDebounce from 'react-use/lib/useDebounce';
1716

17+
import {useOptimisticNavigationData} from '~/hooks/useOptimisticNavigationData';
1818
import {cn} from '~/lib/utils';
1919

20+
import type {AppliedFilter} from './SortFilterLayout';
21+
2022
import {Checkbox} from '../ui/Checkbox';
2123
import {Input} from '../ui/Input';
2224
import {Label} from '../ui/Label';
23-
import {type AppliedFilter, FILTER_URL_PREFIX} from './SortFilterLayout';
25+
import {FILTER_URL_PREFIX} from './SortFilterLayout';
2426

2527
export function DefaultFilter(props: {
2628
appliedFilters: AppliedFilter[];
@@ -29,70 +31,106 @@ export function DefaultFilter(props: {
2931
const {appliedFilters, option} = props;
3032
const [params] = useSearchParams();
3133
const [prefetchPage, setPrefetchPage] = useState<null | string>(null);
32-
const navigate = useNavigate();
33-
const navigation = useNavigation();
3434
const location = useLocation();
3535
const addFilterLink = getFilterLink(option.input as string, params, location);
3636
const appliedFilter = getAppliedFilter(option, appliedFilters);
37-
const isNavigationPending = navigation.state !== 'idle';
3837

39-
const getRemoveFilterLink = useCallback(() => {
38+
const removeFilterLink = useMemo(() => {
4039
if (!appliedFilter) {
41-
return null;
40+
return location.pathname;
4241
}
4342
return getAppliedFilterLink(appliedFilter, params, location);
4443
}, [appliedFilter, params, location]);
4544

46-
const handleToggleFilter = useCallback(() => {
47-
if (appliedFilter) {
48-
const removeFilterLink = getRemoveFilterLink();
49-
if (removeFilterLink) {
50-
navigate(removeFilterLink, {
51-
preventScrollReset: true,
52-
replace: true,
53-
});
54-
}
55-
return;
56-
}
57-
58-
navigate(addFilterLink, {
59-
preventScrollReset: true,
60-
replace: true,
61-
});
62-
}, [addFilterLink, appliedFilter, navigate, getRemoveFilterLink]);
63-
6445
// Prefetch the page that will be navigated to when the user hovers or touches the filter
6546
const handleSetPrefetch = useCallback(() => {
66-
const removeFilterLink = getRemoveFilterLink();
6747
if (appliedFilter) {
6848
setPrefetchPage(removeFilterLink);
6949
return;
7050
}
7151

7252
setPrefetchPage(addFilterLink);
73-
}, [getRemoveFilterLink, addFilterLink, appliedFilter]);
53+
}, [removeFilterLink, addFilterLink, appliedFilter]);
54+
55+
return (
56+
<div onMouseEnter={handleSetPrefetch} onTouchStart={handleSetPrefetch}>
57+
<FilterCheckbox
58+
addFilterLink={addFilterLink}
59+
filterIsApplied={Boolean(appliedFilter)}
60+
option={option}
61+
removeFilterLink={removeFilterLink}
62+
/>
63+
{prefetchPage && <PrefetchPageLinks page={prefetchPage} />}
64+
</div>
65+
);
66+
}
67+
68+
function FilterCheckbox({
69+
addFilterLink,
70+
filterIsApplied,
71+
option,
72+
removeFilterLink,
73+
}: {
74+
addFilterLink: string;
75+
filterIsApplied: boolean;
76+
option: Filter['values'][0];
77+
removeFilterLink: string;
78+
}) {
79+
const navigate = useNavigate();
80+
const optionId = option.id;
81+
const {optimisticData, pending} = useOptimisticNavigationData<{
82+
isFilterChecked: boolean;
83+
}>(optionId);
84+
const {optimisticData: clearFilters} =
85+
useOptimisticNavigationData<boolean>('clear-all-filters');
86+
87+
// Use optimistic checked state while the navigation is pending
88+
if (optimisticData) {
89+
filterIsApplied = optimisticData.isFilterChecked;
90+
}
91+
// Here we can optimistically clear all filters
92+
else if (clearFilters) {
93+
filterIsApplied = false;
94+
}
95+
96+
const handleToggleFilter = useCallback(() => {
97+
const navigateTo = filterIsApplied ? removeFilterLink : addFilterLink;
98+
const optimisticChecked = !filterIsApplied;
99+
100+
navigate(navigateTo, {
101+
preventScrollReset: true,
102+
replace: true,
103+
state: {
104+
optimisticData: {
105+
isFilterChecked: optimisticChecked,
106+
},
107+
optimisticId: optionId,
108+
},
109+
});
110+
}, [filterIsApplied, removeFilterLink, addFilterLink, navigate, optionId]);
74111

75112
return (
76113
<div
77114
className={cn([
78115
'flex items-center gap-2',
79-
isNavigationPending && 'lg:animate-pulse',
116+
// If the navigation is pending, animate after a delay
117+
// to avoid flickering when navigation is fast
118+
pending && 'pointer-events-none animate-pulse delay-500',
80119
])}
81-
onMouseEnter={handleSetPrefetch}
82-
onTouchStart={handleSetPrefetch}
83120
>
84121
<Checkbox
85-
checked={Boolean(appliedFilter)}
86-
id={option.id}
122+
checked={filterIsApplied}
123+
id={optionId}
87124
onCheckedChange={handleToggleFilter}
88125
/>
89126
<Label
90-
className="w-full cursor-pointer lg:transition-opacity lg:hover:opacity-70"
91-
htmlFor={option.id}
127+
className={cn([
128+
'w-full cursor-pointer lg:transition-opacity lg:hover:opacity-70',
129+
])}
130+
htmlFor={optionId}
92131
>
93132
{option.label}
94133
</Label>
95-
{prefetchPage && <PrefetchPageLinks page={prefetchPage} />}
96134
</div>
97135
);
98136
}

0 commit comments

Comments
 (0)