-
-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
render
throws Promise
for parameterized lazy components
#55
Comments
Basically what you are doing here const NameComponent lazy(async () => ('./x.js')); In the above const promiseCache = {};
const App = ({ id }) => {
const [name, setName] = useState(() => {
if (!promiseCache[id]) {
return (promiseCache[id] = getNameById(id).then(name => {
delete promiseCache[id];
setName(name);
}))
} else {
return promiseCache[id].then(name => {
setName(name);
})
}
})
if (name.then) throw name;
return <NameComponent name={name} />
} So with I hope this helps! EDIT: with signals I have created a wrapper in the past that is in RFC, in case you want to try something like that https://github.com/preactjs/signals/compare/async-resource?expand=1 EDIT2: I also made a small demo in the past how this approach can help Preact look at resumed hydration because it can hydrate up until a thrown promise and resume after. The useApi part abstracts that latter throw/fetch mechanism |
@JoviDeCroock's answer is the correct one - this isn't really what const cache = new Map();
function cachedLazy(load, parameters = []) {
const cacheKey = parameters.join('\n'); // or JSON.stringify or whatever you prefer
const cached = cache.get(cacheKey);
if (cached) return;
const comp = lazy(() => load(...parameters));
cache.set(cacheKey, comp);
return comp;
} Usage: function ParameterizedLazyComponent({ id }: { id: number }): VNode {
const Comp3 = cachedLazy(async (id) => {
const name = await getNameById(id); // Call an async function with a prop value.
return () => <div>Hello, {name}!</div>;
}, [id]);
return <Comp3 />;
} |
Thank you both for the quick response. I think I'm understanding now. I didn't realize my tree was getting rendered twice from Is const Comp = lazy(async () => await import('./my-component.js')); I get the impression I'm using the wrong primitive for what I want, but I'm not seeing an obvious alternative. I was able to get something working with a modified form of your const cache = new Map();
function cachedLazy(load, parameters = []) {
const cacheKey = parameters.join('\n'); // or JSON.stringify or whatever you prefer
const cached = cache.get(cacheKey);
if (cached) {
cache.delete(cacheKey); // Remove from the cache when read.
return cached;
}
const comp = lazy(() => load(...parameters));
cache.set(cacheKey, comp);
return comp;
} Not sure how useful or actionable this issue is, feel free to close if it isn't. My immediate thought of potential outcomes from this would be:
|
I encourage you to re-read my message, your use-case is most definitely supported but it's an in-render process.
My example is simplified but you can create a top-level
especially the |
Ah ok, IIUC, you're saying I really shouldn't be using const map = new Map<number, string>();
function ParameterizedLazyComponent({ id }: { id: number }): VNode {
const name = map.get(id);
// If we didn't previously cache a name, then we need to do the async work to get it.
// We throw the `Promise` so the nearest `<Suspense />` waits and rerenders when it is resolved.
// On subsequent renders, `name` is found.
if (!name) {
throw getNameById(id).then((name) => { map.set(id, name); });
}
return <div>Hello, {name}!</div>;
} Am I following this correctly? Is there any documentation on this While this seems to work, I noticed that your approach is caching the I tried an alternative approach with function ParameterizedLazyComponent({ id }: { id: number }): VNode {
const [ name, setName ] = useState<string | undefined>();
if (!name) {
throw getNameById(id).then((n) => { setName(n); });
}
return <div>Hello, {name}!</div>;
} Unfortunately this doesn't actually work. The component renders three times (two from Regarding the memory leak, I also tried deleting the item from the cache after it is read, but that doesn't work because there are three renders, not two. Deleting the cache item after the first read breaks the third render. I suspect the number of renders wouldn't be consistent anyways due to a varying number and structure of other components suspending. So I'm still not clear on how to deal with the memory leak there? To back up a bit, my actual objective is a static-site generator with Preact/JSX as the templating engine (called
It feels like maybe the better approach is to do a fetch-then-render architecture so I avoid the Based on Preact's documentation, React's note that data fetching is not really supported with Suspense, and your own blog post it seems like data fetching with Sorry, I'm starting to ramble and talk about higher-level problems I need to solve for |
Well if you want to make this into a reusable piece it is most definitely possible, i.e. like the linked Yes, your component is execute multiple times...
There is a different option here where you for instance allow your users to provide a function that asynchronously gets the data, some loader-like pattern as Data-fetching libraries like urql indeed implement throwing Promises, I think one of the personal reasons why I have not written a lot about I feel your pain here though hence I am very open to ideate with you. Btw, have you checked out fresh might be a good source of inspiration as well |
Refs #16. This got a little involved, but I learned a lot in the process here. The general pipeline is: 1. Perform an `async` read of the `getting_started.md` file. 2. Parse the markdown with `gray-matter` to separate the frontmatter and markdown body. 3. Parse the markdown body with `marked` and transform it into HTML. 4. Validate the frontmatter with a `zod` schema. 5. Render the page in Preact. This relies on a "fetch-then-render" approach, where all the data is loaded up front asynchronously _and then_ rendered afterwards. I initially tried a "fetch-as-you-render" approach with `<Suspense />` but found it to be a bit lacking. Preact's `<Suspense />` implementation is still experimental, and currently intended for lazy loading components. Data loading with `<Suspense />` is not really a well-trodden path and does not have defined primitives, either in Preact or React. Most React frameworks seem to define their own data loading primitives which I was initially hoping to avoid, letting users structure their own rendering process. For now, the easiest option is to fetch data up front and render afterwards. This isn't great performance and I'm not totally satisfied with it, but it's good enough for now. In the future, hopefully we can revisit async Preact and come away with a better path forward. [See discussion in `preact-ssr-prepass`.](preactjs/preact-ssr-prepass#55)
I've been experimenting with Preact SSR and Suspense and tried to use
preact-ssr-prepass
but foundrender
would always throw aPromise
. I minified my test case and discovered this has to do with precise usage oflazy
. I have a complete minimal repro here.Essentially, this repo makes three attempts at defining a lazy component evaluated with Suspense. The first attempt looks like this:
This attempt renders as you would expect, but also is kind of unnecessary. Let's get a little more complex with attempt 2.
In this attempt we've moved the
lazy
call inside the component function to provide a bit more encapsulation. This attempt fails at render time and throws aPromise
object directly with no error message. Not sure exactly what's wrong with this pattern, but clearly puttinglazy
inside the function breaks it. Maybelazy
can't be called at render time?Let's try attempt 3, which is really just a justification for why you'd want to do this in the first place:
This is the same as attempt 2, except it actually does some meaningful async work. This also fails with the same thrown
Promise
. Ultimately this is really what I want to do, invoke an async operation with a parameter which comes from a function prop. The only way I can see of to do this is to move thelazy
call inside the component so it closes over the prop. However this pattern appears to breakpreact-ssr-prepass
.lazy
doesn't appear to provide any arguments to its callback, so I don't see any other obvious way of getting component prop data into the async operation.I am new to Preact (and not terribly familiar with the React ecosystem in general) so apologies if this has a well-known answer. This feels like a rendering bug given that it is throwing a
Promise
without any error message. If there's a different pattern for developing parameterized lazy components, please let me know. As it stands, I don't see an obvious workaround here which does what I need it to do.The text was updated successfully, but these errors were encountered: