Skip to content

Instantly share code, notes, and snippets.

@likern
Created September 10, 2020 18:15
Show Gist options
  • Select an option

  • Save likern/9bc003967a6dca5a9d791aebce0d9898 to your computer and use it in GitHub Desktop.

Select an option

Save likern/9bc003967a6dca5a9d791aebce0d9898 to your computer and use it in GitHub Desktop.
import React, {
useCallback,
useEffect,
useState,
FunctionComponent,
ComponentClass,
useMemo,
useRef,
ReactNode
} from 'react';
import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated';
import { View, StyleSheet, ViewStyle, StyleProp, Text } from 'react-native';
type Operation = 'round';
type Precision = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
const isRound = (num: number, precision: Precision) => {
'worklet';
//return decimalPlaces >= 0 &&
// +num.toFixed(decimalPlaces) === num;
var p = Math.pow(10, precision);
return Math.round(num * p) / p === num;
};
const decimalAdjust = (type: Operation, num: number, precision: Precision) => {
'worklet';
if (isRound(num, precision)) return num;
var p = Math.pow(10, precision);
var e = Number.EPSILON * num * p;
return Math[type](num * p + e) / p;
};
const round = (num: number, precision: Precision) => {
'worklet';
return decimalAdjust('round', num, precision);
};
const areEqual = (num1: number, num2: number, precision: Precision) => {
'worklet';
return round(num1, precision) === round(num2, precision);
};
type ComponentType<P> = FunctionComponent<P> | ComponentClass<P>;
type RowType<Key, Props extends {}> = [Key, Props, ComponentType<Props>];
export interface StringConvertable {
toString(): string;
}
type InfiniteListProps<Key, Props> = {
style?: StyleProp<ViewStyle>;
nextPage(lastRow: RowType<Key, Props> | null): Promise<RowType<Key, Props>[]>;
};
const createMap = <Key, Value>() => {
return new Map<Key, Value>();
};
const createEmptyObject = () => ({});
interface PageProps {
children?: ReactNode;
style?: StyleProp<ViewStyle>;
}
const Page = (props: PageProps) => {
return (
<Animated.View style={[styles.page, props.style]}>
{props.children}
</Animated.View>
);
};
const InfiniteList = <Key extends StringConvertable | string | number, Props>({
style,
nextPage
}: InfiniteListProps<Key, Props>) => {
const [internalState, setInternalState] = useState(createEmptyObject);
const rerender = useCallback(() => {
setInternalState({});
}, [setInternalState]);
const [pageCache] = useState<Map<number, React.ReactElement<any>>>(createMap);
const lastRowRef = useRef<RowType<Key, Props> | null>(null);
const lastPageRef = useRef<number | null>(null);
const addNextPageToCache = useCallback(async () => {
const rows = await nextPage(lastRowRef.current);
const rowsLength = rows.length;
if (rowsLength > 0) {
const elements: React.ReactElement<Props>[] = new Array(rowsLength);
for (let i = 0; i < rowsLength; i++) {
const [key, props, component] = rows[i];
let elementKey: string | number =
typeof key !== 'number' && typeof key !== 'string'
? key.toString()
: key;
const finalProps = {
key: elementKey,
...props
};
const element = React.createElement(component, finalProps);
elements[i] = element;
if (i + 1 === rowsLength) {
lastRowRef.current = rows[i];
}
}
const pageKey = (lastPageRef.current ?? 0) + 1;
lastPageRef.current = pageKey;
const page = React.createElement(
Page,
{
key: pageKey
},
elements
);
pageCache.set(pageKey, page);
}
}, [pageCache, nextPage]);
useEffect(() => {
(async () => {
if (lastPageRef.current === null) {
await addNextPageToCache();
rerender();
}
})();
}, [addNextPageToCache, rerender]);
const onTopReached = useCallback(() => {}, []);
const onEndReached = useCallback(() => {
(async () => {
await addNextPageToCache();
rerender();
})();
}, [rerender, addNextPageToCache]);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
const isTopReached = areEqual(event.contentOffset.y, 0, 2);
if (isTopReached) {
onTopReached && onTopReached();
} else {
const isEndReached = areEqual(
event.layoutMeasurement.height + event.contentOffset.y,
event.contentSize.height,
2
);
if (isEndReached) {
onEndReached && onEndReached();
}
}
},
onBeginDrag: (_event) => {},
onEndDrag: (_event) => {},
onMomentumBegin: (_event) => {},
onMomentumEnd: (_event) => {}
});
const children = useMemo(() => {
console.log(`useMemo called, cache size: ${pageCache.size}`);
const pages: React.ReactElement<PageProps>[] = [];
for (const element of pageCache.values()) {
pages.push(element);
}
return pages;
}, [internalState, pageCache]);
return (
<Animated.ScrollView
style={[{ width: '100%', height: '100%' }, style]}
onScroll={scrollHandler}
>
{children}
</Animated.ScrollView>
);
};
const styles = StyleSheet.create({
page: {
width: '100%',
height: '100%'
}
});
interface TextRowProps {
id: number;
title: string;
style?: StyleProp<ViewStyle>;
}
const TextRow = (props: TextRowProps) => {
return (
<View
key={props.id}
style={[
{ minHeight: 56, alignItems: 'center', justifyContent: 'center' },
props.style
]}
>
<Text style={{ fontSize: 18 }}>{props.title}</Text>
</View>
);
};
const nextPage = async (lastRow: RowType<number, TextRowProps> | null) => {
const lastRowId: number = lastRow?.[0] ?? 0;
const count = 15;
const rows: RowType<StringConvertable, TextRowProps>[] = [];
for (let i = 1; i <= count; i++) {
const id = i + lastRowId;
const taskProps: TextRowProps = {
id,
title: `Task ${id}`
};
const row: RowType<StringConvertable, TextRowProps> = [
id,
taskProps,
TextRow
];
rows.push(row);
}
return rows;
};
export const TestInfiniteList = () => {
return <InfiniteList nextPage={nextPage} />;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment