react-native-wishlist
Version:
The fastest List component for React Native.
215 lines (191 loc) • 6.71 kB
text/typescript
import { useMemo } from 'react';
import { useOnFlushCallback, useScheduleSyncUp } from './OrchestratorBinding';
import { useWishlistContext } from './WishlistContext';
export type Item = {
key: string;
};
export type UpdateJob<T extends Item> = (datacopy: DataCopy<T>) => unknown;
export interface DataCopy<T extends Item> {
getIndex: (key: string) => number | undefined;
deque: Array<T>;
at: (index: number) => T | undefined;
length: () => number;
set: (key: string, value: T) => void;
setItem: (key: string, value: T) => void;
get: (key: string) => T | undefined;
forKey: (key: string) => T | undefined;
setAt: (index: number, value: T) => void;
push: (value: T) => void;
unshift: (value: T) => void;
applyChanges: (pendingUpdates: Array<UpdateJob<T>>) => Set<string>;
isTrackingChanges: boolean;
dirtyKeys: Set<string>;
}
export interface Data<T extends Item> {
update: (job: UpdateJob<T>, callback?: (results: unknown) => void) => void;
at: (index: number) => T | undefined;
length: () => number;
forKey: (key: string) => T | undefined;
pendingUpdates: Array<UpdateJob<T>>;
__currentlyRenderedCopy: DataCopy<T>;
__nextCopy: DataCopy<T>;
}
export function useInternalWishlistData<T extends Item>(
wishlistId: string,
initialData: Array<T>,
) {
const scheduleSyncUp = useScheduleSyncUp(wishlistId);
const data = useMemo(() => {
return () => {
'worklet';
if (!global.dataCtx) {
global.dataCtx = {};
}
if (!global.dataCtx[wishlistId]) {
function createItemsDataStructure(initialData: Array<T>) {
// classes doesn't work :(
// TODO it can be implmented so that all ops are O(log n)
const thiz: DataCopy<T> = {
deque: initialData,
getIndex: function getIndex(key: string) {
// That's linear but can be log n (only for testing)
for (let i = 0; i < this.deque.length; ++i) {
if (this.deque[i].key === key) {
return i;
}
}
return undefined;
},
at: function at(index: number) {
if (index == undefined || index >= this.length() || index < 0) {
return undefined;
}
return this.deque[index];
},
length: function length() {
return this.deque.length;
},
forKey: function forKey(key: string) {
const index = this.getIndex(key);
if (!index) {
return undefined;
}
return this.at(index);
},
get: function get(key: string) {
return this.forKey(key);
},
setItem: function setItem(key: string, value: T) {
const index = this.getIndex(key);
this.setAt(index!, value);
},
set: function set(key: string, value: T) {
return this.setItem(key, value);
},
setAt: function setAt(index: number, value: T) {
this.deque[index] = value;
if (this.isTrackingChanges) {
this.dirtyKeys.add(value.key);
}
},
push: function push(value: T) {
this.deque.push(value);
},
unshift: function unshift(value: T) {
this.deque.unshift(value);
},
applyChanges: function applyChanges(pendingUpdates) {
this.isTrackingChanges = true;
for (let updateJob of pendingUpdates) {
updateJob(this);
}
this.isTrackingChanges = false;
const res = this.dirtyKeys;
this.dirtyKeys = new Set();
return res;
},
dirtyKeys: new Set<string>(),
isTrackingChanges: false,
};
return thiz;
}
function deepClone<ObjT>(x: ObjT): ObjT {
if ((x as any).map != null) {
return (x as any).map((ele: unknown) => deepClone(ele));
}
if (typeof x === 'object') {
const res: any = {};
for (let key of Object.keys(x as any)) {
res[key] = deepClone((x as any)[key]);
}
return res;
}
return x;
}
const initialDataCopy__next = deepClone(initialData);
const initialDataCopy__cur = deepClone(initialData);
const __nextCopy = createItemsDataStructure(initialDataCopy__next);
const __currentlyRenderedCopy =
createItemsDataStructure(initialDataCopy__cur);
const pendingUpdates: Array<UpdateJob<T>> = [];
function update(
updateJob: UpdateJob<T>,
callback?: (result: any) => void,
) {
updateJob(__nextCopy);
pendingUpdates.push((dataCopy: DataCopy<T>) => {
const result = updateJob(dataCopy);
callback?.(result);
});
scheduleSyncUp();
}
function at(index: number) {
return __currentlyRenderedCopy.at(index);
}
function length() {
return __currentlyRenderedCopy.length();
}
function forKey(key: string) {
return __currentlyRenderedCopy.forKey(key);
}
const internalData = {
update,
at,
forKey,
length,
__currentlyRenderedCopy,
__nextCopy,
pendingUpdates,
};
global.dataCtx[wishlistId] = internalData;
}
return global.dataCtx[wishlistId] as Data<T>;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useOnFlushCallback((viewportObserver) => {
'worklet';
const pendingUpdates = data().pendingUpdates;
const pendingUpdatesCopy = pendingUpdates.splice(0, pendingUpdates.length);
const dirty =
data().__currentlyRenderedCopy.applyChanges(pendingUpdatesCopy);
const window = viewportObserver.getAllVisibleItems();
// Right now we only support adding items but it can be easily extended
const newIndex = data().__currentlyRenderedCopy.getIndex(window[0].key);
viewportObserver.updateIndices(newIndex!);
const dirtyItems = [];
let i = 0;
for (let item of window) {
if (dirty.has(item.key)) {
dirtyItems.push(i);
}
i++;
}
viewportObserver.markItemsDirty(dirtyItems);
}, wishlistId);
return data as () => Data<T>;
}
export function useData<T extends Item>() {
const { data } = useWishlistContext();
return data as () => Data<T>;
}