Created
February 23, 2021 17:23
-
-
Save 0xorial/f59e32f612b1fc7cc1bc7b756b6d4c3c to your computer and use it in GitHub Desktop.
useAsyncEffect
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {useEffect, useRef, useState} from 'react'; | |
export type AsyncEffectFuncContext = { | |
wrap: <T>(p: Promise<T>) => Promise<T>; | |
}; | |
export type AsyncEffectFunc = (p: AsyncEffectFuncContext) => Promise<void>; | |
export function useAsyncEffect( | |
createGenerator: AsyncEffectFunc, | |
deps: React.DependencyList, | |
errorCallback: ((error: any) => void) | null = null | |
): {trigger: () => Promise<void>} { | |
const [counter, setCounter] = useState(0); | |
const pendingPromises = useRef( | |
new LinkedList<{ | |
resolve: () => void; | |
reject: (e: Error) => void; | |
}>() | |
); | |
useEffect(() => { | |
let isCanceled = false; | |
const p = { | |
wrap: function<T>(t: Promise<T>) { | |
return new Promise<T>((resolve, reject) => { | |
t.then(x => { | |
if (!isCanceled) { | |
resolve(x); | |
} | |
}).catch(x => { | |
if (!isCanceled) { | |
reject(x); | |
} | |
}); | |
}); | |
} | |
}; | |
createGenerator(p) | |
.then(() => { | |
// async calls should have been canceled through 'wrap' function | |
const promises = pendingPromises.current.toArray(); | |
for (let promise of promises) { | |
promise.resolve(); | |
} | |
}) | |
.catch(e => { | |
console.error(e); | |
if (errorCallback) errorCallback(e); | |
const promises = pendingPromises.current.toArray(); | |
for (let promise of promises) { | |
promise.reject(e); | |
} | |
}); | |
return () => { | |
isCanceled = true; | |
}; | |
}, [...deps, counter]); | |
return { | |
trigger: () => { | |
setCounter(x => x + 1); | |
return new Promise<void>((resolve, reject) => { | |
const r = pendingPromises.current.add({ | |
resolve: () => { | |
r.detachSelf(); | |
resolve(); | |
}, | |
reject: () => { | |
r.detachSelf(); | |
reject(); | |
} | |
}); | |
}); | |
} | |
}; | |
} | |
export class HeadNode<T> { | |
public next: LinkedListNode<T> | TailNode<T>; | |
constructor() { | |
this.next = new TailNode(this); | |
} | |
} | |
// tslint:disable-next-line:max-classes-per-file | |
export class TailNode<T> { | |
public previous: LinkedListNode<T> | HeadNode<T>; | |
constructor(head: HeadNode<T>) { | |
this.previous = head; | |
} | |
} | |
// tslint:disable-next-line:max-classes-per-file | |
export class LinkedListNode<T> { | |
public next: LinkedListNode<T> | TailNode<T> | null = null; | |
public previous: LinkedListNode<T> | HeadNode<T> | null = null; | |
public readonly item: T; | |
constructor(item: T) { | |
this.item = item; | |
} | |
public detachSelf() { | |
if (!this.next && !this.previous) { | |
throw new Error('node is not attached'); | |
} | |
if (this.next) { | |
this.next.previous = this.previous; | |
} | |
if (this.previous) { | |
this.previous.next = this.next; | |
} | |
this.next = null; | |
this.previous = null; | |
} | |
public attachAfter(node: LinkedListNode<T> | HeadNode<T>) { | |
if (this.next || this.previous) { | |
throw new Error('Node is inserted elsewhere'); | |
} | |
this.next = node.next; | |
this.previous = node; | |
if (node.next) { | |
node.next.previous = this; | |
} | |
node.next = this; | |
} | |
public attachBefore(node: LinkedListNode<T> | TailNode<T>) { | |
if (!node.previous) { | |
throw new Error('no previous node found.'); | |
} | |
this.attachAfter(node.previous); | |
} | |
public isAttached() { | |
return this.next !== undefined; | |
} | |
} | |
// tslint:disable-next-line:max-classes-per-file | |
export class LinkedList<T> { | |
public head: HeadNode<T>; | |
public tail: TailNode<T>; | |
constructor() { | |
this.head = new HeadNode<T>(); | |
this.tail = this.head.next as TailNode<T>; | |
} | |
public add(item: T): LinkedListNode<T> { | |
const newNode = new LinkedListNode(item); | |
newNode.attachAfter(this.tail.previous); | |
return newNode; | |
} | |
public getItems(): T[] { | |
const result: T[] = []; | |
this.forEach(item => { | |
result.push(item); | |
}); | |
return result; | |
} | |
public forEach(callback: (item: T, node: LinkedListNode<T>) => void) { | |
let current = this.head.next; | |
while (current !== this.tail) { | |
// if item is not tail it is always a node | |
const item = current as LinkedListNode<T>; | |
callback(item.item, item); | |
if (!item.next) { | |
throw new Error('badly attached item found.'); | |
} | |
current = item.next; | |
} | |
} | |
public hasItems() { | |
return this.head.next !== this.tail; | |
} | |
public getLastItem() { | |
if (!this.hasItems()) { | |
throw new Error('no items in list.'); | |
} | |
return this.head.next as LinkedListNode<T>; | |
} | |
toArray() { | |
const result: T[] = []; | |
this.forEach(x => { | |
result.push(x); | |
}); | |
return result; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment