1import * as React from 'react'; 2 3// utility function for capturing all push and pop stack events 4// components can subscribe to internal state to push and pop their own views depending on use case 5// e.g a modal stack, a screen stack, a toast stack, etc 6 7export type IStackEvent = 'pushstart' | 'pushend' | 'popstart' | 'popend' | 'replace'; 8 9export type StackItemStatus = 'pushing' | 'popping' | 'settled'; 10 11export type ListenerFn<T> = ({ 12 items, 13}: { 14 action: IStackEvent; 15 key: string; 16 items: StackItem<T>[]; 17 getItemByKey: (key: string) => T | undefined; 18}) => void; 19 20export type StackItem<T> = T & { key: string; status: StackItemStatus }; 21export type IReplaceOptions<T> = T & { replaceAmount?: number; key?: string }; 22export type IPushOptions<T> = T & { key?: string }; 23export interface IStack<T> { 24 push: (pushOptions: IPushOptions<T>) => Promise<string>; 25 pop: (amount?: number) => Promise<string[]>; 26 replace: (replaceOptions: IReplaceOptions<T>) => Promise<any>; 27 onPushEnd: (key: string) => void; 28 onPopEnd: (key: string) => void; 29 subscribe: (listener: ListenerFn<T>) => () => void; 30 getState: () => { 31 items: StackItem<T>[]; 32 getItemByKey: (key: string) => T | undefined; 33 }; 34} 35 36const generateRouteKey = () => `${new Date().getTime()}`; 37 38export function createAsyncStack<T>(): IStack<T> { 39 let keys: string[] = []; 40 const lookup: Record<string, StackItem<T>> = {}; 41 42 const pushResolvers: Record<string, Function> = {}; 43 const popResolvers: Record<string, Function> = {}; 44 45 let listeners: any[] = []; 46 47 function push(pushOptions: IPushOptions<T>) { 48 const key = pushOptions.key || generateRouteKey(); 49 50 if (keys.includes(key)) { 51 return Promise.resolve(key); 52 } 53 54 keys.push(key); 55 56 lookup[key] = { 57 ...pushOptions, 58 key, 59 status: 'pushing', 60 }; 61 62 const promise = new Promise<string>((resolve) => { 63 pushResolvers[key] = resolve; 64 }); 65 66 emit('pushstart', key); 67 68 return promise; 69 } 70 71 function onPushEnd(key: string) { 72 const item = lookup[key]; 73 74 if (item) { 75 item.status = 'settled'; 76 77 emit('pushend', key); 78 79 const resolver = pushResolvers[key]; 80 81 if (resolver) { 82 resolver(key); 83 } 84 } 85 } 86 87 function pop(amount = 1, startIndex = 0) { 88 const promises = []; 89 90 if (amount === -1) { 91 // pop them all 92 amount = keys.length; 93 } 94 95 for (let i = 1; i <= amount; i++) { 96 const key = keys[keys.length - startIndex - i]; 97 const item = lookup[key]; 98 99 if (item) { 100 item.status = 'popping'; 101 102 const promise = new Promise((resolve) => { 103 popResolvers[key] = resolve; 104 }); 105 106 promises.push(promise); 107 emit('popstart', key); 108 } 109 } 110 111 return Promise.all(promises) as Promise<string[]>; 112 } 113 114 function onPopEnd(key: string) { 115 keys = keys.filter((k) => k !== key); 116 117 const resolver = popResolvers[key]; 118 119 if (resolver) { 120 resolver(key); 121 } 122 123 delete popResolvers[key]; 124 delete pushResolvers[key]; 125 126 emit('popend', key); 127 } 128 129 async function replace(replaceOptions: IReplaceOptions<T>) { 130 const itemsToPop = replaceOptions.replaceAmount != null ? replaceOptions.replaceAmount : 1; 131 132 const promise2 = await push(replaceOptions); 133 const promise1 = await pop(itemsToPop, 1); 134 135 return Promise.all([promise2, promise1]); 136 } 137 138 function subscribe(listener: any) { 139 listeners.push(listener); 140 141 return () => { 142 listeners = listeners.filter((l) => l !== listener); 143 }; 144 } 145 146 function emit(action: IStackEvent, key: string) { 147 listeners.forEach((listener) => { 148 const state = getState(); 149 listener({ ...state, key, action }); 150 }); 151 } 152 153 function getItemByKey(key: string) { 154 return lookup[key]; 155 } 156 157 function getState() { 158 const items = keys.map((key) => lookup[key]); 159 160 return { 161 items, 162 getItemByKey, 163 }; 164 } 165 166 return { 167 push, 168 onPushEnd, 169 pop, 170 onPopEnd, 171 replace, 172 subscribe, 173 getState, 174 }; 175} 176 177export function useStackItems<T>(stack: IStack<T>) { 178 const [items, setItems] = React.useState(() => stack.getState().items); 179 180 React.useEffect(() => { 181 const unsubscribe = stack.subscribe(({ items }) => { 182 setItems(items); 183 }); 184 185 return () => { 186 unsubscribe && unsubscribe(); 187 }; 188 }, [stack]); 189 190 return items; 191} 192