Skip to content

Commit e35a094

Browse files
committed
ref(feedback): Replace react-virtualized with
`@tanstack/react-virtual` in Feedback
1 parent e9dfe8a commit e35a094

File tree

6 files changed

+276
-216
lines changed

6 files changed

+276
-216
lines changed

static/app/components/feedback/list/feedbackList.tsx

Lines changed: 50 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import {Fragment, useMemo, useRef} from 'react';
2-
import type {ListRowProps} from 'react-virtualized';
3-
import {
4-
AutoSizer,
5-
CellMeasurer,
6-
InfiniteLoader,
7-
List as ReactVirtualizedList,
8-
} from 'react-virtualized';
1+
import {Fragment, useMemo} from 'react';
92
import styled from '@emotion/styled';
103

114
import waitingForEventImg from 'sentry-images/spot/waiting-for-event.svg';
@@ -15,157 +8,94 @@ import ErrorBoundary from 'sentry/components/errorBoundary';
158
import FeedbackListHeader from 'sentry/components/feedback/list/feedbackListHeader';
169
import FeedbackListItem from 'sentry/components/feedback/list/feedbackListItem';
1710
import useFeedbackQueryKeys from 'sentry/components/feedback/useFeedbackQueryKeys';
11+
import InfiniteListItems from 'sentry/components/infiniteList/infiniteListItems';
12+
import InfiniteListState from 'sentry/components/infiniteList/infiniteListState';
1813
import LoadingIndicator from 'sentry/components/loadingIndicator';
1914
import {t} from 'sentry/locale';
2015
import {space} from 'sentry/styles/space';
21-
import useFetchInfiniteListData from 'sentry/utils/api/useFetchInfiniteListData';
16+
import uniqueBy from 'sentry/utils/array/uniqBy';
2217
import type {FeedbackIssueListItem} from 'sentry/utils/feedback/types';
2318
import {useListItemCheckboxContext} from 'sentry/utils/list/useListItemCheckboxState';
24-
import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
19+
import {useInfiniteApiQuery} from 'sentry/utils/queryClient';
2520

26-
// Ensure this object is created once as it is an input to
27-
// `useVirtualizedList`'s memoization
28-
const cellMeasurer = {
29-
fixedWidth: true,
30-
minHeight: 24,
31-
};
32-
33-
function NoFeedback({title, subtitle}: {subtitle: string; title: string}) {
21+
function NoFeedback() {
3422
return (
35-
<Wrapper>
36-
<img src={waitingForEventImg} alt="No feedback found spot illustration" />
37-
<EmptyMessage>{title}</EmptyMessage>
38-
<p>{subtitle}</p>
39-
</Wrapper>
23+
<NoFeedbackWrapper>
24+
<img src={waitingForEventImg} alt={t('A person waiting for a phone to ring')} />
25+
<NoFeedbackMessage>{t('Inbox Zero')}</NoFeedbackMessage>
26+
<p>{t('You have two options: take a nap or be productive.')}</p>
27+
</NoFeedbackWrapper>
4028
);
4129
}
4230

4331
export default function FeedbackList() {
4432
const {listQueryKey} = useFeedbackQueryKeys();
45-
const {
46-
isFetchingNextPage,
47-
isFetchingPreviousPage,
48-
isLoading, // If anything is loaded yet
49-
getRow,
50-
isRowLoaded,
51-
issues,
52-
loadMoreRows,
53-
hits,
54-
} = useFetchInfiniteListData<FeedbackIssueListItem>({
33+
const queryResult = useInfiniteApiQuery<FeedbackIssueListItem[]>({
5534
queryKey: listQueryKey ?? ['infinite', ''],
56-
uniqueField: 'id',
5735
enabled: Boolean(listQueryKey),
5836
});
5937

38+
const issues = useMemo(
39+
() => uniqueBy(queryResult.data?.pages.flatMap(([pageData]) => pageData) ?? [], 'id'),
40+
[queryResult.data?.pages]
41+
);
6042
const checkboxState = useListItemCheckboxContext({
61-
hits,
43+
hits: issues.length,
6244
knownIds: issues.map(issue => issue.id),
6345
queryKey: listQueryKey,
6446
});
6547

66-
const listRef = useRef<ReactVirtualizedList>(null);
67-
68-
const deps = useMemo(() => [isLoading, issues.length], [isLoading, issues.length]);
69-
const {cache, updateList} = useVirtualizedList({
70-
cellMeasurer,
71-
ref: listRef,
72-
deps,
73-
});
74-
75-
const renderRow = ({index, key, style, parent}: ListRowProps) => {
76-
const item = getRow({index});
77-
if (!item) {
78-
return null;
79-
}
80-
81-
return (
82-
<ErrorBoundary mini key={key}>
83-
<CellMeasurer cache={cache} columnIndex={0} parent={parent} rowIndex={index}>
84-
<FeedbackListItem
85-
feedbackItem={item}
86-
isSelected={checkboxState.isSelected(item.id)}
87-
onSelect={() => {
88-
checkboxState.toggleSelected(item.id);
89-
}}
90-
style={style}
91-
/>
92-
</CellMeasurer>
93-
</ErrorBoundary>
94-
);
95-
};
96-
9748
return (
9849
<Fragment>
9950
<FeedbackListHeader {...checkboxState} />
10051
<FeedbackListItems>
101-
<InfiniteLoader
102-
isRowLoaded={isRowLoaded}
103-
loadMoreRows={loadMoreRows}
104-
rowCount={hits}
52+
<InfiniteListState
53+
queryResult={queryResult}
54+
backgroundUpdatingMessage={() => null}
55+
loadingMessage={() => <LoadingIndicator />}
10556
>
106-
{({onRowsRendered, registerChild}) => (
107-
<AutoSizer onResize={updateList}>
108-
{({width, height}) => (
109-
<ReactVirtualizedList
110-
deferredMeasurementCache={cache}
111-
height={height}
112-
noRowsRenderer={() =>
113-
isLoading ? (
114-
<LoadingIndicator />
115-
) : (
116-
<NoFeedback
117-
title={t('Inbox Zero')}
118-
subtitle={t('You have two options: take a nap or be productive.')}
119-
/>
120-
)
121-
}
122-
onRowsRendered={onRowsRendered}
123-
overscanRowCount={5}
124-
ref={e => {
125-
listRef.current = e;
126-
registerChild(e);
57+
<InfiniteListItems<FeedbackIssueListItem>
58+
estimateSize={() => 24}
59+
queryResult={queryResult}
60+
itemRenderer={({item}) => (
61+
<ErrorBoundary mini>
62+
<FeedbackListItem
63+
feedbackItem={item}
64+
isSelected={checkboxState.isSelected(item.id)}
65+
onSelect={() => {
66+
checkboxState.toggleSelected(item.id);
12767
}}
128-
rowCount={issues.length}
129-
rowHeight={cache.rowHeight}
130-
rowRenderer={renderRow}
131-
width={width}
13268
/>
133-
)}
134-
</AutoSizer>
135-
)}
136-
</InfiniteLoader>
137-
<FloatingContainer style={{top: '2px'}}>
138-
{isFetchingPreviousPage ? (
139-
<Tooltip title={t('Loading more feedback...')}>
140-
<LoadingIndicator mini />
141-
</Tooltip>
142-
) : null}
143-
</FloatingContainer>
144-
<FloatingContainer style={{bottom: '2px'}}>
145-
{isFetchingNextPage ? (
146-
<Tooltip title={t('Loading more feedback...')}>
147-
<LoadingIndicator mini />
148-
</Tooltip>
149-
) : null}
150-
</FloatingContainer>
69+
</ErrorBoundary>
70+
)}
71+
emptyMessage={() => <NoFeedback />}
72+
loadingMoreMessage={() => (
73+
<Centered>
74+
<Tooltip title={t('Loading more feedback...')}>
75+
<LoadingIndicator mini />
76+
</Tooltip>
77+
</Centered>
78+
)}
79+
loadingCompleteMessage={() => null}
80+
/>
81+
</InfiniteListState>
15182
</FeedbackListItems>
15283
</Fragment>
15384
);
15485
}
15586

15687
const FeedbackListItems = styled('div')`
157-
display: grid;
88+
display: flex;
89+
flex-direction: column;
15890
flex-grow: 1;
159-
min-height: 300px;
91+
padding-bottom: ${space(0.5)};
16092
`;
16193

162-
const FloatingContainer = styled('div')`
163-
position: absolute;
94+
const Centered = styled('div')`
16495
justify-self: center;
16596
`;
16697

167-
const Wrapper = styled('div')`
168-
display: flex;
98+
const NoFeedbackWrapper = styled('div')`
16999
padding: ${space(4)} ${space(4)};
170100
flex-direction: column;
171101
align-items: center;
@@ -175,12 +105,9 @@ const Wrapper = styled('div')`
175105
@media (max-width: ${p => p.theme.breakpoints.sm}) {
176106
font-size: ${p => p.theme.fontSize.md};
177107
}
178-
position: relative;
179-
top: 50%;
180-
transform: translateY(-50%);
181108
`;
182109

183-
const EmptyMessage = styled('div')`
110+
const NoFeedbackMessage = styled('div')`
184111
font-weight: ${p => p.theme.fontWeight.bold};
185112
color: ${p => p.theme.gray400};
186113

static/app/components/feedback/list/feedbackListItem.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type {CSSProperties} from 'react';
21
import styled from '@emotion/styled';
32

43
import {ActorAvatar} from 'sentry/components/core/avatar/actorAvatar';
@@ -29,8 +28,6 @@ interface Props {
2928
feedbackItem: FeedbackIssueListItem;
3029
isSelected: 'all-selected' | boolean;
3130
onSelect: (isSelected: boolean) => void;
32-
ref?: React.Ref<HTMLDivElement>;
33-
style?: CSSProperties;
3431
}
3532

3633
function useIsSelectedFeedback({feedbackItem}: {feedbackItem: FeedbackIssueListItem}) {
@@ -41,7 +38,7 @@ function useIsSelectedFeedback({feedbackItem}: {feedbackItem: FeedbackIssueListI
4138
return feedbackId === feedbackItem.id;
4239
}
4340

44-
function FeedbackListItem({feedbackItem, isSelected, onSelect, style, ref}: Props) {
41+
export default function FeedbackListItem({feedbackItem, isSelected, onSelect}: Props) {
4542
const organization = useOrganization();
4643
const isOpen = useIsSelectedFeedback({feedbackItem});
4744
const {feedbackHasReplay} = useReplayCountForFeedbacks();
@@ -53,7 +50,7 @@ function FeedbackListItem({feedbackItem, isSelected, onSelect, style, ref}: Prop
5350
const hasComments = feedbackItem.numComments > 0;
5451

5552
return (
56-
<CardSpacing ref={ref} style={style}>
53+
<CardSpacing>
5754
<LinkedFeedbackCard
5855
data-selected={isOpen}
5956
to={{
@@ -169,8 +166,6 @@ function FeedbackListItem({feedbackItem, isSelected, onSelect, style, ref}: Prop
169166
);
170167
}
171168

172-
export default FeedbackListItem;
173-
174169
const LinkedFeedbackCard = styled(Link)`
175170
position: relative;
176171
padding: ${space(1)} ${space(3)} ${space(1)} ${space(1.5)};

0 commit comments

Comments
 (0)