-
Notifications
You must be signed in to change notification settings - Fork 911
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
[labs/ssr] do we need special ssr only async decorators? #2469
Comments
Doesn't seem crazy to me :) How about something like this? import {whenSsr} from 'lit/ssr.js';
const dataPromise = fetch(...).then(...);
// When server-side rendering, wait for dataPromise to resolve and render that.
// When client-side rendering, render "Loading..." initially, then dataPromise after it resolves.
html`<p>${whenSsr(dataPromise, until(dataPromise, "Loading..."))}</p>`
export const whenSsr = (ssrPromise, value) => {
value['$lit-ssr-blocking-promise$'] = ssrPromise;
return value;
} During SSR, we'd check for the special property, and if it's present, block rendering until it resolves, and render the resolved result: async function* renderValue(value: unknown, renderInfo: RenderInfo) {
...
const blockingPromise = value['$lit-ssr-blocking-promise$'];
if (blockingPromise !== undefined) {
yield *renderValue(await blockingPromise, renderInfo);
}
} You could maybe imagine directives directly setting the import {until} from 'lit/directives/until.js';
import {blocksSsr} from 'lit/ssr.js';
html`<p>${until(blocksSsr(dataPromise), "Loading...")}</p>` With an implementation something like this: export class UntilDirective extends AsyncDirective {
...
render(...args: Array<unknown>) {
this['$lit-ssr-blocking-promise$'] = args.find((x) => x['$flagged-by-blocks-ssr$']);
return args.find((x) => !isPromise(x)) ?? noChange;
}
} (We probably don't want to do that specifically because it would add overhead to every |
if we only have a promise on the server-side and none on the client side then it would work sort of like this? import { readFile } from 'fs/promises';
const serverPromise = new Promise(resolve => {
readFile(new URL('../assets/burger-menu.svg', import.meta.url), 'utf8').then(
read => resolve(read.toString())
);
});
// When server-side rendering, wait for dataPromise to resolve and render that.
// When client-side it will render treat it static content
html`<p>${whenSsr(serverPromise)}</p>` ssr output would be <p><svg>...</svg></p> so no parts inside? 🤔 |
I think in that example you'd also need By the way, are you writing templates that you only need to run on the server? Obviously in your example, the Also, this is probably stating the obvious, but the simplest way you could handle async data is by taking it as a parameter to your template and doing the async work before you start rendering: const myTemplate = (data) => html`<p>${data}</p>`
const myHttpHandler = async (ctx) {
const data = await getSomeData();
const ssrResult = render(myTemplate(data));
context.body = Readable.from(ssrResult);
} I think the advantage of a |
yes indeed
yes, the example above would need unsafeHTML... right now I moved all async stuff outside the template, which works in most cases... import { readFile } from 'fs/promises';
import { unsafeHTML } from 'lit/...';
function getSvg(name) {
return new Promise(resolve => {
readFile(new URL(`../assets/${name}.svg`, import.meta.url), 'utf8').then(
read => resolve(unsafeHTML(read.toString()))
);
});
});
const myTemplate = ({ svgName }) => html`<p>${whenSsr(getSvg(svgName))}</p>` |
Hey this looks really cool! I'm thinking from a user's perspective, we'd want to avoid requiring extra syntax and steps, so what do you think of this:
The user's goals here:
|
I think we have a few items to design together here to make this work properly. The most important part to me is around imports. We need SSR-only logic to live in SSR-only modules, and I'd like to do that without custom code transformations. The best thing I can think of right now is for client-code with SSR-only deps to use guarded dynamic imports that can be dead-code eliminated in various tools: import {isSsr, serverUntil} from '@lit-labs/ssr-client';
const renderSvgServer = async (svgName) => {
const {readFile} = await import('fs/promises');
return serverUntil(readFile(...));
};
const renderSvg = isSsr ? renderSvgServer : () => noChange;
export const myTemplate = ({svgName}) => html`<div>${renderSvg(svgName)</div>`; I'm sure this is wrong in several ways, but the idea is this would run fine without any transformations, and that you could run this through Terser and replace isSsr with |
for now the isSsr can probably be worked around by checking for things not (yet) supported in SSR, like window.navigator? |
hmmm I just ran into this again 😅 not loading any server side code statically totally makes sense 👍 In this case I can not work around it... as the async server code depends on a property of the element 😭 export class OpenGraphOverview extends LitElement {
static properties = {
urls: { type: String },
inputDir: { type: String, attribute: 'input-dir' },
};
render() {
const iframeUrl = this.url.endsWith('/')
? `${this.url}index.opengraph.html`
: this.url.replace(/\.html$/, '.opengraph.html');
return html`
<div>
<a href="vscode://${hydrateAbleUrlToSourceFilePath(url, this.inputDir)}">
<server-icon icon="solid/laptop-code"></server-icon>
</a>
<iframe src="${iframeUrl}" loading="lazy" width="1200" height="628"></iframe>
</div>
`;
}
const isSsr = window && window.navigator ? false : true;
async function serverUrlToSourceFilePath(url, inputDir) {
const { urlToSourceFilePath } = await import('@rocket/engine');
// urlToSourceFilePath is a function that searched the file system for potential matches
// e.g. it's async & needs access to the file system
return serverUntil(urlToSourceFilePath(url, inputDir));
}
const hydrateAbleUrlToSourceFilePath = isSsr ? serverUrlToSourceFilePath : () => noChange; on the server side I can then render it like this export default () => html`
<opengraph-overview
urls="${JSON.stringify(urls)}"
input-dir=${new URL('./', import.meta.url).href}
></opengraph-overview>
`; which result in this <opengraph-overview
urls="["/","/docs/","/docs/setup/"]"
input-dir="file:///html/rocket/site/pages/">
<!--lit-node 1-->
</opengraph-overview> which I guess could then be hydrated 🤔 Soooo I think this will need a few steps to make it work 😅 1. some way to execute an async code via ssrAs a first step having a Impact: No user code workarounds possible => this is actually blocking me right now 2. Best practices for how to make hydratable server functionsShow and explain how static imports to server needing function break hydration. So if you want to hydrate you need to use a dynamic import for server functions. Impact: You can already do this yourself right now - so not blocking. 3. StreamlineOnce we have it working we can look into improving the developer experience.
Impact: It's a nice to have for now. |
At the moment, I have the same problem — fetching data from the DB (usually this is an async operation 🙂) I have two examples of async server-side rendering:
The second option makes it possible to get a completely isomorphic components, but with a certain set of hydration bugs (the branch needs to be updated 🔥) Hope this helps you move forward ! |
Can we work around any of this by making use of the Task labs package? Extend this to have an |
Just wanted to chime in as well that being able to do some (exclusive) import { html, LitElement } from 'lit';
import { getProducts } from '../services/products.js';
import '../components/card.js';
export default class ProductsPage extends LitElement {
constructor() {
super();
this.products = [];
}
async connectedCallback() {
super.connectedCallback();
this.products = await getProducts();
}
render() {
const { products } = this;
return html`
${
products.map((product) => {
const { title, thumbnail } = product;
return html`
<app-card
title="${idx + 1}) ${title}"
thumbnail="${thumbnail}"
></app-card>
`;
})
}
`;
}
} In the above case maybe the presence / hint of an |
Task should be the kind of thing that can help here. If we can choose server vs client implementations of |
Thanks @justinfagnani , will take a look into Task we well. |
I've been working on adding a server only hook to the ReactiveController interface (called My PR also enables extending import {Task} from '@lit/task';
//...
class ServerLoadedTask extends Task implements ServerController {
get serverUpdateComplete() {
this.run();
return this.taskComplete;
}
} In the future we'd ideally like the bake this into the Task package itself. But for now it would be possible in userland. Other patterns I've explored are the need for SSR only directives. With a On the client, in order to not destroy the server rendered content, it's ideal to use I'd appreciate anyone taking a look and leaving feedback if something stands out. |
Description
I for example quite often inlining svgs when generating html
expected output
actual output
now there are 2 issues
some (crazy) ideas
what do you think?
Additional info
a simplified version
expected
actual
The text was updated successfully, but these errors were encountered: