Skip to content

Commit f288443

Browse files
feat: added forwardRef support
#3
1 parent f4fb7ec commit f288443

File tree

1 file changed

+166
-156
lines changed

1 file changed

+166
-156
lines changed

src/index.tsx

Lines changed: 166 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { useRef, useState } from 'react';
1+
import React, { MutableRefObject, useRef, useState } from 'react';
22
import {
33
ActivityIndicator,
4+
FlatList as FlatListType,
45
FlatListProps,
56
ScrollViewProps,
67
StyleSheet,
@@ -66,176 +67,185 @@ type Props<T> = Omit<FlatListProps<T>, 'maintainVisibleContentPosition'> & {
6667
* - doesn't accept `ListHeaderComponent` via prop, since it is occupied by `HeaderLoadingIndicator`
6768
* Set `showDefaultLoadingIndicators` to use `ListHeaderComponent`.
6869
*/
69-
const BidirectionalFlatList = <T extends any>(props: Props<T>) => {
70-
const {
71-
activityIndicatorColor = 'black',
72-
data,
73-
enableAutoscrollToTop,
74-
autoscrollToTopThreshold = 100,
75-
FooterLoadingIndicator,
76-
HeaderLoadingIndicator,
77-
ListHeaderComponent,
78-
ListFooterComponent,
79-
onEndReached = () => Promise.resolve(),
80-
onEndReachedThreshold = 10,
81-
onScroll,
82-
onStartReached = () => Promise.resolve(),
83-
onStartReachedThreshold = 10,
84-
showDefaultLoadingIndicators = true,
85-
} = props;
86-
87-
const [onStartReachedInProgress, setOnStartReachedInProgress] = useState(
88-
false
89-
);
90-
const [onEndReachedInProgress, setOnEndReachedInProgress] = useState(false);
91-
92-
const onStartReachedTracker = useRef<Record<number, boolean>>({});
93-
const onEndReachedTracker = useRef<Record<number, boolean>>({});
94-
95-
const onStartReachedInPromise = useRef<Promise<void> | null>(null);
96-
const onEndReachedInPromise = useRef<Promise<void> | null>(null);
97-
98-
const maybeCallOnStartReached = () => {
99-
// If onStartReached has already been called for given data length, then ignore.
100-
if (data?.length && onStartReachedTracker.current[data.length]) {
101-
return;
102-
}
103-
104-
if (data?.length) {
105-
onStartReachedTracker.current[data.length] = true;
106-
}
107-
108-
setOnStartReachedInProgress(true);
109-
const p = () => {
110-
return new Promise<void>((resolve) => {
111-
onStartReachedInPromise.current = null;
112-
setOnStartReachedInProgress(false);
113-
resolve();
114-
});
115-
};
70+
const BidirectionalFlatList = React.forwardRef(
71+
(
72+
// TODO: Fix typescript generics for ref forwarding.
73+
props: Props<any>,
74+
ref:
75+
| ((instance: FlatListType | null) => void)
76+
| MutableRefObject<FlatListType | null>
77+
| null
78+
) => {
79+
const {
80+
activityIndicatorColor = 'black',
81+
data,
82+
enableAutoscrollToTop,
83+
autoscrollToTopThreshold = 100,
84+
FooterLoadingIndicator,
85+
HeaderLoadingIndicator,
86+
ListHeaderComponent,
87+
ListFooterComponent,
88+
onEndReached = () => Promise.resolve(),
89+
onEndReachedThreshold = 10,
90+
onScroll,
91+
onStartReached = () => Promise.resolve(),
92+
onStartReachedThreshold = 10,
93+
showDefaultLoadingIndicators = true,
94+
} = props;
95+
const [onStartReachedInProgress, setOnStartReachedInProgress] = useState(
96+
false
97+
);
98+
const [onEndReachedInProgress, setOnEndReachedInProgress] = useState(false);
99+
100+
const onStartReachedTracker = useRef<Record<number, boolean>>({});
101+
const onEndReachedTracker = useRef<Record<number, boolean>>({});
116102

117-
if (onEndReachedInPromise.current) {
118-
onEndReachedInPromise.current.finally(() => {
103+
const onStartReachedInPromise = useRef<Promise<void> | null>(null);
104+
const onEndReachedInPromise = useRef<Promise<void> | null>(null);
105+
106+
const maybeCallOnStartReached = () => {
107+
// If onStartReached has already been called for given data length, then ignore.
108+
if (data?.length && onStartReachedTracker.current[data.length]) {
109+
return;
110+
}
111+
112+
if (data?.length) {
113+
onStartReachedTracker.current[data.length] = true;
114+
}
115+
116+
setOnStartReachedInProgress(true);
117+
const p = () => {
118+
return new Promise<void>((resolve) => {
119+
onStartReachedInPromise.current = null;
120+
setOnStartReachedInProgress(false);
121+
resolve();
122+
});
123+
};
124+
125+
if (onEndReachedInPromise.current) {
126+
onEndReachedInPromise.current.finally(() => {
127+
onStartReachedInPromise.current = onStartReached().then(p);
128+
});
129+
} else {
119130
onStartReachedInPromise.current = onStartReached().then(p);
120-
});
121-
} else {
122-
onStartReachedInPromise.current = onStartReached().then(p);
123-
}
124-
};
125-
126-
const maybeCallOnEndReached = () => {
127-
// If onEndReached has already been called for given data length, then ignore.
128-
if (data?.length && onEndReachedTracker.current[data.length]) {
129-
return;
130-
}
131-
132-
if (data?.length) {
133-
onEndReachedTracker.current[data.length] = true;
134-
}
135-
136-
setOnEndReachedInProgress(true);
137-
const p = () => {
138-
return new Promise<void>((resolve) => {
139-
onStartReachedInPromise.current = null;
140-
setOnStartReachedInProgress(false);
141-
resolve();
142-
});
131+
}
143132
};
144133

145-
if (onStartReachedInPromise.current) {
146-
onStartReachedInPromise.current.finally(() => {
147-
onEndReachedInPromise.current = onEndReached().then(p);
148-
});
149-
} else {
150-
onEndReachedInPromise.current = onEndReached().then(p);
151-
}
152-
};
153-
154-
const handleScroll: ScrollViewProps['onScroll'] = (event) => {
155-
// Call the parent onScroll handler, if provided.
156-
onScroll?.(event);
157-
158-
const offset = event.nativeEvent.contentOffset.y;
159-
const visibleLength = event.nativeEvent.layoutMeasurement.height;
160-
const contentLength = event.nativeEvent.contentSize.height;
161-
162-
// Check if scroll has reached either start of end of list.
163-
const isScrollAtStart = offset < onStartReachedThreshold;
164-
const isScrollAtEnd =
165-
contentLength - visibleLength - offset < onEndReachedThreshold;
166-
167-
if (isScrollAtStart) {
168-
maybeCallOnStartReached();
169-
}
170-
171-
if (isScrollAtEnd) {
172-
maybeCallOnEndReached();
173-
}
174-
};
175-
176-
const renderHeaderLoadingIndicator = () => {
177-
if (!showDefaultLoadingIndicators) {
178-
if (ListHeaderComponent) {
179-
return <ListHeaderComponent />;
134+
const maybeCallOnEndReached = () => {
135+
// If onEndReached has already been called for given data length, then ignore.
136+
if (data?.length && onEndReachedTracker.current[data.length]) {
137+
return;
138+
}
139+
140+
if (data?.length) {
141+
onEndReachedTracker.current[data.length] = true;
142+
}
143+
144+
setOnEndReachedInProgress(true);
145+
const p = () => {
146+
return new Promise<void>((resolve) => {
147+
onStartReachedInPromise.current = null;
148+
setOnStartReachedInProgress(false);
149+
resolve();
150+
});
151+
};
152+
153+
if (onStartReachedInPromise.current) {
154+
onStartReachedInPromise.current.finally(() => {
155+
onEndReachedInPromise.current = onEndReached().then(p);
156+
});
180157
} else {
181-
return null;
158+
onEndReachedInPromise.current = onEndReached().then(p);
182159
}
183-
}
160+
};
184161

185-
if (!onStartReachedInProgress) return null;
162+
const handleScroll: ScrollViewProps['onScroll'] = (event) => {
163+
// Call the parent onScroll handler, if provided.
164+
onScroll?.(event);
186165

187-
if (HeaderLoadingIndicator) {
188-
return <HeaderLoadingIndicator />;
189-
}
166+
const offset = event.nativeEvent.contentOffset.y;
167+
const visibleLength = event.nativeEvent.layoutMeasurement.height;
168+
const contentLength = event.nativeEvent.contentSize.height;
190169

191-
return (
192-
<View style={styles.indicatorContainer}>
193-
<ActivityIndicator size={'small'} color={activityIndicatorColor} />
194-
</View>
195-
);
196-
};
170+
// Check if scroll has reached either start of end of list.
171+
const isScrollAtStart = offset < onStartReachedThreshold;
172+
const isScrollAtEnd =
173+
contentLength - visibleLength - offset < onEndReachedThreshold;
197174

198-
const renderFooterLoadingIndicator = () => {
199-
if (!showDefaultLoadingIndicators) {
200-
if (ListFooterComponent) {
201-
return <ListFooterComponent />;
202-
} else {
203-
return null;
175+
if (isScrollAtStart) {
176+
maybeCallOnStartReached();
177+
}
178+
179+
if (isScrollAtEnd) {
180+
maybeCallOnEndReached();
181+
}
182+
};
183+
184+
const renderHeaderLoadingIndicator = () => {
185+
if (!showDefaultLoadingIndicators) {
186+
if (ListHeaderComponent) {
187+
return <ListHeaderComponent />;
188+
} else {
189+
return null;
190+
}
191+
}
192+
193+
if (!onStartReachedInProgress) return null;
194+
195+
if (HeaderLoadingIndicator) {
196+
return <HeaderLoadingIndicator />;
204197
}
205-
}
206198

207-
if (!onEndReachedInProgress) return null;
199+
return (
200+
<View style={styles.indicatorContainer}>
201+
<ActivityIndicator size={'small'} color={activityIndicatorColor} />
202+
</View>
203+
);
204+
};
208205

209-
if (FooterLoadingIndicator) {
210-
return <FooterLoadingIndicator />;
211-
}
206+
const renderFooterLoadingIndicator = () => {
207+
if (!showDefaultLoadingIndicators) {
208+
if (ListFooterComponent) {
209+
return <ListFooterComponent />;
210+
} else {
211+
return null;
212+
}
213+
}
214+
215+
if (!onEndReachedInProgress) return null;
216+
217+
if (FooterLoadingIndicator) {
218+
return <FooterLoadingIndicator />;
219+
}
220+
221+
return (
222+
<View style={styles.indicatorContainer}>
223+
<ActivityIndicator size={'small'} color={activityIndicatorColor} />
224+
</View>
225+
);
226+
};
212227

213228
return (
214-
<View style={styles.indicatorContainer}>
215-
<ActivityIndicator size={'small'} color={activityIndicatorColor} />
216-
</View>
229+
<>
230+
<FlatList
231+
{...props}
232+
ref={ref}
233+
progressViewOffset={50}
234+
ListHeaderComponent={renderHeaderLoadingIndicator}
235+
ListFooterComponent={renderFooterLoadingIndicator}
236+
onEndReached={null}
237+
onScroll={handleScroll}
238+
// @ts-ignore
239+
maintainVisibleContentPosition={{
240+
autoscrollToTopThreshold: enableAutoscrollToTop
241+
? autoscrollToTopThreshold
242+
: undefined,
243+
minIndexForVisible: 1,
244+
}}
245+
/>
246+
</>
217247
);
218-
};
219-
220-
return (
221-
<>
222-
<FlatList
223-
{...props}
224-
progressViewOffset={50}
225-
ListHeaderComponent={renderHeaderLoadingIndicator}
226-
ListFooterComponent={renderFooterLoadingIndicator}
227-
onEndReached={null}
228-
onScroll={handleScroll}
229-
// @ts-ignore
230-
maintainVisibleContentPosition={{
231-
autoscrollToTopThreshold: enableAutoscrollToTop
232-
? autoscrollToTopThreshold
233-
: undefined,
234-
minIndexForVisible: 1,
235-
}}
236-
/>
237-
</>
238-
);
239-
};
248+
}
249+
);
240250

241251
export { BidirectionalFlatList as FlatList };

0 commit comments

Comments
 (0)