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 if (item.status === 'pushing') { 76 item.status = 'settled'; 77 } 78 79 emit('pushend', key); 80 81 const resolver = pushResolvers[key]; 82 83 if (resolver) { 84 resolver(key); 85 } 86 } 87 } 88 89 function pop(amount = 1, startIndex = 0) { 90 const promises = []; 91 92 if (amount === -1) { 93 // pop them all 94 amount = keys.length; 95 } 96 97 for (let i = 1; i <= amount; i++) { 98 const key = keys[keys.length - startIndex - i]; 99 const item = lookup[key]; 100 101 if (item) { 102 item.status = 'popping'; 103 104 const promise = new Promise((resolve) => { 105 popResolvers[key] = resolve; 106 }); 107 108 promises.push(promise); 109 emit('popstart', key); 110 } 111 } 112 113 return Promise.all(promises) as Promise<string[]>; 114 } 115 116 function onPopEnd(key: string) { 117 keys = keys.filter((k) => k !== key); 118 119 const resolver = popResolvers[key]; 120 121 if (resolver) { 122 resolver(key); 123 } 124 125 delete popResolvers[key]; 126 delete pushResolvers[key]; 127 128 emit('popend', key); 129 } 130 131 async function replace(replaceOptions: IReplaceOptions<T>) { 132 const itemsToPop = replaceOptions.replaceAmount != null ? replaceOptions.replaceAmount : 1; 133 134 const promise2 = await push(replaceOptions); 135 const promise1 = await pop(itemsToPop, 1); 136 137 return Promise.all([promise2, promise1]); 138 } 139 140 function subscribe(listener: any) { 141 listeners.push(listener); 142 143 return () => { 144 listeners = listeners.filter((l) => l !== listener); 145 }; 146 } 147 148 function emit(action: IStackEvent, key: string) { 149 listeners.forEach((listener) => { 150 const state = getState(); 151 listener({ ...state, key, action }); 152 }); 153 } 154 155 function getItemByKey(key: string) { 156 return lookup[key]; 157 } 158 159 function getState() { 160 const items = keys.map((key) => lookup[key]); 161 162 return { 163 items, 164 getItemByKey, 165 }; 166 } 167 168 return { 169 push, 170 onPushEnd, 171 pop, 172 onPopEnd, 173 replace, 174 subscribe, 175 getState, 176 }; 177} 178 179export function useStackItems<T>(stack: IStack<T>) { 180 const [items, setItems] = React.useState(() => stack.getState().items); 181 182 React.useEffect(() => { 183 const unsubscribe = stack.subscribe(({ items }) => { 184 setItems(items); 185 }); 186 187 return () => { 188 unsubscribe && unsubscribe(); 189 }; 190 }, [stack]); 191 192 return items; 193} 194