Skip to content

Instantly share code, notes, and snippets.

@kgtkr
Created September 25, 2022 09:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kgtkr/01a6065c6767ae3baba04ca15d0e97e8 to your computer and use it in GitHub Desktop.
Save kgtkr/01a6065c6767ae3baba04ca15d0e97e8 to your computer and use it in GitHub Desktop.
suspense loader
import React from "react";
function shallowEqual<T extends readonly unknown[]>(a: T, b: T) {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
type Cache<T> =
| {
type: "loading";
promise: Promise<T>;
}
| {
type: "success";
value: T;
}
| {
type: "failure";
error: unknown;
};
type Entry<Params, Result> = {
params: Params;
cache: Cache<Result>;
count: number;
};
export class LoaderStore<Params extends readonly unknown[], Result> {
private entries: Array<Entry<Params, Result>>;
constructor(private loader: (params: Params) => Promise<Result>) {
this.entries = [];
}
private getEntry(params: Params) {
return this.entries.find((item) => shallowEqual(item.params, params));
}
private updateCache(params: Params, cache: Cache<Result>) {
const index = this.entries.findIndex((item) =>
shallowEqual(item.params, params)
);
if (index === -1) {
return;
}
this.entries[index].cache = cache;
}
preload(params: Params): Entry<Params, Result> {
const entry = this.getEntry(params);
if (entry) {
return entry;
}
const promise = this.loader(params)
.then((value) => {
this.updateCache(params, { type: "success", value });
return value;
})
.catch((error) => {
this.updateCache(params, { type: "failure", error });
throw error;
});
const newEntry: Entry<Params, Result> = {
params,
cache: { type: "loading", promise },
count: 0,
};
this.entries.push(newEntry);
return newEntry;
}
load(params: Params): Result {
const { cache } = this.preload(params);
switch (cache.type) {
case "loading":
throw cache.promise;
case "success":
return cache.value;
case "failure":
throw cache.error;
}
}
add(params: Params) {
const entry = this.preload(params);
entry.count++;
}
remove(params: Params) {
const index = this.entries.findIndex((item) =>
shallowEqual(item.params, params)
);
if (index === -1) {
return;
}
const entry = this.entries[index];
entry.count--;
if (entry.count === 0) {
this.entries.splice(index, 1);
}
}
}
export default function useLoader<Params extends readonly unknown[], Result>(
loaderStore: LoaderStore<Params, Result>,
params: Params
) {
React.useEffect(() => {
loaderStore.add(params);
return () => {
loaderStore.remove(params);
};
}, [loaderStore, ...params]);
return loaderStore.load(params);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment