Skip to content

Instantly share code, notes, and snippets.

@honzabrecka
Created January 21, 2022 20:44
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 honzabrecka/180a6a3ada7fdb2776515d0205ff7845 to your computer and use it in GitHub Desktop.
Save honzabrecka/180a6a3ada7fdb2776515d0205ff7845 to your computer and use it in GitHub Desktop.
import React, { useState, useCallback, useMemo, Suspense } from "react";
const [rejected, resolved, pending] = ["rejected", "resolved", "pending"];
export const wrapAsyncResource = (factory, lazy = true) => {
let promise;
let state = [pending, undefined];
const startPromise = () => {
promise = factory();
promise
.then((data) => {
state = [resolved, data];
})
.catch((error) => {
state = [rejected, error];
});
};
if (!lazy) {
startPromise();
}
return {
getValue() {
if (!promise) {
startPromise();
}
const [status, result] = state;
if (status === pending) {
throw promise;
}
if (status === rejected) {
throw result;
}
return result;
},
getPromise() {
if (!promise) {
startPromise();
}
return promise;
}
};
};
const wrapDynamicAsyncResource = (factory, lazy) => {
const resources = {};
const createIfUndefined = (id) => {
if (resources[id]) return;
resources[id] = wrapAsyncResource(factory, lazy);
};
return {
getValue(id) {
createIfUndefined(id);
return resources[id].getValue();
},
getPromise(id) {
createIfUndefined(id);
return resources[id].getPromise();
}
};
};
export const useResource = ({
factory,
lazy = true,
initialArgs = [],
wrapper = wrapAsyncResource
}) => {
const [resource, setResource] = useState(() =>
wrapper(() => factory(...initialArgs), lazy)
);
const refresh = useCallback(
(...args) => {
setResource(wrapper(() => factory(...args), lazy));
},
[factory, lazy, wrapper]
);
return [resource, refresh];
};
//
// fake, just for demonstration how to pass another hook (it's result) inside useResource
const useApi = () => {
return {};
};
const fetchChart = async (id) => {
// return fetch(`api/chart/${id}`);
return { series: [{ source: "bar" }] };
};
const fetchChartCollection = async (id) => {
// return fetch(`api/chart-collection/${id}`);
return [1, 2, 3];
};
const ChartInner = ({ id, resource, chartFieldCollections }) => {
const { series } = resource.getValue();
console.log(chartFieldCollections);
// somehow collect required "sources" from series
const sources = useMemo(() => {
// each inner resource is lazy, so is created on demand and fetched when needed
// many charts can ask for one "source" (resource) and it will be fetched just once
// All the hard work (is all ready?) will be done by react :)
return series.map(({ source }) => chartFieldCollections.getValue(source));
}, [series, chartFieldCollections]);
// render chart as now we have all the data...
console.log({ id, series, sources });
return <canvas />;
};
const Chart = ({ id, chartFieldCollections }) => {
// in real app it would be useChart wrapping this
const [resource] = useResource({ factory: fetchChart, initialArgs: [id] });
return (
<Suspense fallback="loading">
<ChartInner
id={id}
chartFieldCollections={chartFieldCollections}
resource={resource}
/>
</Suspense>
);
};
export default function App() {
const api = useApi();
// no caching, just state uplifting so one resource can be shared
// across children
const [chartFieldCollections] = useResource({
factory: fetchChartCollection,
initialArgs: [api],
wrapper: wrapDynamicAsyncResource
});
return (
<div className="App">
<Chart id={"foo"} chartFieldCollections={chartFieldCollections} />
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment