Skip to content
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

Support for Server Component Skeletons #243

Open
jrstrunk opened this issue Feb 16, 2025 · 0 comments
Open

Support for Server Component Skeletons #243

jrstrunk opened this issue Feb 16, 2025 · 0 comments

Comments

@jrstrunk
Copy link

jrstrunk commented Feb 16, 2025

Hello! As previously discussed, it would be nice to be able to specify a skeleton for server components. I use the word skeleton and not "loading" because it is not really primarily for loading. Though it can also be used as a loading state for server components, it is also very useful in places where JS is not available (like when prefetching HTML, web scrapers (and SEO stuff), page fragment links, for users with JS disabled, etc.).

I know this may not directly correlate to v5 code, but in v4 adding support for this required just one line of code:

var LustreServerComponent = class extends HTMLElement {
  static get observedAttributes() {
    return ["route"];
  }
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = '<slot name="skeleton"></slot>'; // <- this is the one new line!
    this.#observer = new MutationObserver((mutations) => {
    // ...
    });
  }
  // ...
}

Then, it can be used like:

pub fn render_with_skeleton(name: String, skeleton: element.Element(msg)) {
  element.element(
    "lustre-server-component",
    [server_component.route("/" <> name)],
    [html.div([attribute.attribute("slot", "skeleton")], [skeleton])],
  )
}

For user-discoverability and ease, I might suggest adding a skeleton function to the server_component module, which could be like so:

pub fn skeleton(element: element.Element(msg)) {
  html.div([attribute.attribute("slot", "skeleton")], [skeleton])
}

which could then easily be used like:

  element.element(
    "lustre-server-component",
    [server_component.route("/" <> name)],
    [server_component.skeleton(html.p([], [html.text("Spooky!")]))],
  )

If you think this is a good practice, the server_component.component function could also be changed to take a skeleton element list (could be passed as [] if not wanted) to encourage users to provide one.

Edit: after working on this further, I have two more things to note:

  1. A skeleton-like stand-in for a server component is already possible with Lustre as-is by rendering something where the server component will eventually go. However, when the server component loads, the entire component will flash momentarily. Adding the skeleton slot allows for a seamless replacement by the server component.
  2. The user needs to provide a place for the skeleton slot to go after the server component is rendered, so they have to include something like the following in the server component. It is not bad, but it is not as clean as I'd like it to be.
pub fn hide_skeleton() {
  html.slot([
    attribute.name("skeleton"),
    attribute.style([#("display", "none")]),
  ])
}

Edit again: if there was a way to have the server statically generate the first state of a server component and then hydrate it later with a real connection (like we can do with client components), that would encompass this plus more. I don't know all the implications of how that would be done, though, and if it is even reasonable because of the quick-changing nature of the data the server component is probably serving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant