Let’s Build Suspense

Introduction to Suspense

Julian Burr introduces his deep admiration for React's Suspense feature, highlighting its importance not just on the client side but even more so on the server. He explains that it is crucial for React server components and hopes to demystify its inner workings during his talk.

The Evolution of Web Rendering

Julian outlines the history of web rendering, starting from static HTML to server-side rendering using technologies like PHP, and advancing to the client-side with JavaScript libraries such as jQuery and Ajax. This evolution eventually led to the creation of single-page applications and, subsequently, meta frameworks like NextJS and Gatsby to address server-side rendering again.

Hydration and React Server Components

The speaker delves into the problem of hydration in single-page applications and introduces how React server components aim to tackle this by optimizing the server-side rendering process. He explains how server components help differentiate between static and dynamic components to minimize client-side bundles, even though they cannot solve server-side computation delays.

Understanding Suspense with Code

Julian begins a live demonstration to explain how Suspense works by building a suspense feature from scratch. He outlines the steps from classical server-side rendering to streaming, emphasizing how suspense allows for better server-side rendering and introduces the concept of out-of-order streaming to enhance user experience.

Implementing Suspense in a Demo Application

The speaker shares a demo application named Notflix and explains its structure. He shows how the application utilizes asynchronous components and server-side rendering through Express. He then illustrates how to introduce streaming and handling loading states using suspense boundaries on the server-side.

Building Custom Suspense Logic

Julian discusses implementing custom JavaScript logic to replace fallback UI with loaded content. He uses custom elements to create a simple way to swap loading states with actual content when data is loaded. This setup involves client-side scripting and a unique approach to mirror React's suspense capabilities.

The Power of Suspense: Conclusion

Finally, Julian demonstrates the completed demo showing how suspense creates a seamless loading experience by displaying immediate visual feedback with loading states. He concludes by emphasizing the power of suspense for both client and server-side rendering, highlighting its importance in modern web applications.

Additional Resources and Q&A

The speaker provides a list of resources for further exploration and invites the audience to delve deeper into the topic. He offers access to the demo code and his slides and opens the floor for questions, encouraging engagement and further discussion about suspense and its applications.

Yes, I wanna talk about suspense and disclaimer front, I'm a huge suspense fanboy, like I love suspense.

The first time they demoed it almost seven years ago now.

So it's been around for a while.

A lot of us know what it is on the client.

For those of you don't, it's basically a React feature that allows you to put boundaries within your application that then allow you to render a fallback UI, usually like a loading state whenever anything underneath those boundaries is still waiting for something asynchronous to happen.

So that could be data fetching, that could be lazy loading JavaScript chunks, anything asynchronous.

And it basically deals with that loading stuff for you.

So that's amazing on the client.

But I think suspense on the server is even more impressive and a lot of people don't really know even what it is, or even if they know what it is, they don't really know how it works.

I would even go further and I would say suspense on the server is what makes React server components possible.

Because without them they couldn't really work the way they work.

So in today's talk, I hope that I can show why I think that is, and demystify a lot of things about what's actually happening under the hood.

To understand why we even need suspense and what it does.

I think we need to take a really quick look at the history of web rendering.

Scott this morning did a very simplified version of it.

I also make a simplified version, but I add a couple of more steps.

I might sound old here, but I hope some people in the room still remember the days when we were writing plain HTML.

Just static HTML out of the box.

Eventually then we, tried to, or we, moved onto the server and say okay, we dynamically generate the HTML, with server side languages like PHP, so that's great.

From a content perspective.

So now we can ship dynamic content.

We can hook into databases and stuff like that.

But it comes at a downside.

We are now computing on the server if there's a lot of computation happening or if the, the database connection is slow or whatever, that server response time can get really slow really quickly, and that's bad for user experience.

So in the early two thousands, JavaScript became more and more popular, and the real trigger for that was jQuery, jQuery and technologies like Ajax, which allowed us to, even after the initial server response, to go back to the server to do more stuff.

And with that combination, we started selectively going into our UI and say okay, let's just load the critical HTML first, and then once that's loaded, we can go back to the server.

We can show loading state, everything is nice.

We can even do page transitions more easily.

All of that kind of stuff.

And that was so convenient that we started doing it more and offloading more and more stuff to the client.

Until we eventually ended up with what we now know as single page applications.

So that's the era of frameworks like React, like Vue, like Angular, where we basically say, okay, we want to build dynamic web apps.

This is the best way to do it from a develop experience perspective, from a user experience perspective, that was like a step back in the sense that the initial load suddenly wasn't really good for the user because with single page applications, what you ship from the server, really just an empty div most of the time.

And then on the client side, you build up that HTML that you inject into that empty div.

So especially at large scale, that initial load can be really frustrating, even though the user experience after that initial load is great.

So very quickly after we introduced single page applications, we realized that and we moved back to the server.

But we wanted to keep that nice develop experience from those frameworks, like React.

So meta frameworks like NextJS, Gatsby, became really popular to get us server side rendering with React.

And what that means is your React app will run on the server first to generate static HTML that gets shipped to the client for the initial load.

But then the framework still takes over on the client and you can do all your fancy stuff that you were doing before and that sounds great as a compromise, but it introduced us to a new problem or it highlighted a problem, that we call hydration.

So again, Scott this morning, showed some really good actual numbers against that.

But basically what that means is because React and frameworks like it is inherently built for the client side.

So anything can be dynamic, in order to do server side rendering, yes, we run your app on the server first to generate the static HTML.

But then we have to run the whole app essentially again on the client to create a virtual copy of that DOM to inject all the dynamic elements.

So that's state, that's event listeners, that's effects and all of that kind of stuff that you can't represent in static HTML and that's really expensive.

So this is where React server components come in.

React server components.

The whole idea is to be able to tell the bundler whether a component is just static and only ever needs to be rendered once, it doesn't have any state, it doesn't have any effects or anything.

And then it can just be omitted from the client side bundle.

It doesn't need to be sent to the client.

It can just render once on the server and then it's done.

Versus explicitly telling the bundler, no, this is actually a dynamic component.

It has an event listener, for example.

So we need to include it in the client side bundle, and need to ship it to the client and need to hydrate that component.

So this can realistically drastically, reduce the client side bundle and the hydration that needs to happen, but it can't really solve another problem.

So what I didn't mention is when we did the server side rendering, we basically got back to the problem that we had with HTML and PHP, which is we are on the server.

So if we do a lot of work on the server, that initial response time can get really slow really quickly again.

Server components themselves don't solve that.

So this is where suspense comes in.

And to understand how suspense does that, we are gonna build suspense from scratch ourselves, hopefully successfully.

But essentially I want to go through the different stages, that I just described with the server-side rendering principles.

So we're gonna start with just what I'm gonna call classical server side rendering, and then we're gonna proof that through something, introducing streaming, and then essentially getting to what suspense allows us to do, which is called out of order streaming.

And hopefully through that journey you can see why it's so powerful and what it enables us to do.

So enough talking, let's write some code.

Not yet.

Cool.

So for the purpose of this, I created a small demo application.

It's a movie app because that's what everyone seems to be building these days for demos.

And I'm not very creative.

It's called Notflix, 'cause it's not like Netflix.

But you can see the, page that we are, that we're trying to build is very simple.

So you have our logo at the top.

Then we have, a title section with the movie title, some meta information, the poster, short movie description.

Then we have a middle section that's showing the cast of the movie.

And we have a bottom section that shows similar movies that we want the user to watch because we never want the user to leave our app.

And if we look at the code for that.

It's also pretty straightforward.

So we have the title component, which is the title section with the poster.

And what we can see here is a couple of things.

For one, we are fetching, the information like we're fetching in the title.

For the purpose of this demo, it doesn't matter where that comes from.

If it's like a third party API or if it's our own database or whatever.

All that matters is it takes time.

It's not instant.

The other thing that we can see, and the details section is very similar.

We have a fetch details that fetches the cast members.

But the other thing that we can see is that we're essentially dealing with server components.

So those components can't have any state, they can't have any effects, but what they can be is asynchronous because we know they're only ever gonna run once.

So that means we can do the data fetching right inside of the component, which is pretty neat.

Now if we look at the server implementation, of the server set of all this, we can see it's basically a standard, like I have a base, a pretty standard express set up here.

So we have our endpoint, and then endpoint that renders an application, renders our asynch components.

And then all we really do is we start concatenating the HTML as a string.

So we start with the duck type, then we go through all of the components, of our app tree.

Again, they can be asynchronous functions.

So we await, the return to get the element, then we render it to a string, and at the end we just concatenate all the strings and then we send all of that to the browser.

And what that looks like is literally what we looked at before.

So this is running that server and that's all good, but it has that.

Problem that I described earlier.

If I click now on any link here, you can see it takes a while to actually change.

There we go.

And that's because the browser needs to wait for your whole response to be finished before it navigates.

So that's bad from a user experience perspective.

How can we improve it?

I already implied, that before we can introduce something that's called streaming.

So streaming has been basically around forever, ever since the internet started becoming a thing.

And basically what it means is you start sending a response from the server immediately and just keep appending to that document rather than the browser having to wait for the whole document to be sent at once.

And frameworks like Express or other server frameworks, do that or allow you to do that very easily.

On the response object, we have a right method and that literally does exactly that.

It just writes to a response stream.

So if we change all of our code, hopefully live coding, it's gonna be fun.

We basically, we don't do the whole do one thing at a time, but whenever it's ready, we write to that response stream.

So we don't need that anymore.

And, and because we've already basically written everything in the end, we just call resend, to tell the browser that the connection can now be closed.

We're basically done.

And that's all you need to change to actually start streaming stuff.

And if we refresh the page now, you can see the, it, the logo loads immediately, which is great.

Some of you may have already noticed and like screaming internally.

The, sections are now out of order.

And that's expected because we're still doing like a promise all here.

We're trying to deal with all the asynchronous components in parallel, which we can't really do now because we're streaming.

So we only ever pending to the documents.

We have to deal with them in sequence.

So what I'm gonna do now to just quickly fix that is using a for loop instead of the promise all.

I know that's not the most efficient way to do it, but I'm gonna try to keep things simple for the sake of the demo.

So this now in sequence goes through all of the children and results them one by one.

And if we refresh the page now, you can see that it does what we want.

So whenever a section is ready, whenever we loaded the stuff from API or database, it will pop in and get appended to the document.

And this is already a lot better.

Like we can see that it's a lot better by clicking on a link.

We get the immediate user feedback, or visual feedback that actually something is happening.

It's still not great though because we are still mostly looking at a blank screen.

Especially imagine the logo wasn't there at the top.

We would literally just be staring at a blank black screen, until the first section loads in.

So how can we improve that?

How can we actually show like a loading state that shows visually, that something is gonna come in?

If we were on the client, what we would do is just use the suspense boundary, right?

So we could do something like this and put a loading state in there.

And now if anything, if title details are similar, are still loading, it would show that fallback UI.

So let's build that on the server.

First we need to create the suspense component.

This is not a TypeScript talk, so anys are absolutely fine.

For the sake of time, I'm not gonna type everything.

And for now, let's just return the fallback, just to see if it's working and we need to import it here.

If we refresh the page, it doesn't like it just.

Oh, contrast isn't great.

Trust me, there's a loading state somewhere there.

That's, it's, much more impressive here.

Damn it.

But yeah, so we're, right now we're rendering loading states.

So how do we actually deal with the suspended components?

So for that, let's just create a simple object, let's call it suspended and.

So we want to be able to keep track of every suspended component for each of the suspense boundaries.

So let's give them unique IDs.

And because these are server com…, like we know this is only rendered once on the server.

Again, to keep it simple, we can do stuff like this, where we just increase that counter, for every instance of the suspense component.

And then we just.

Do that.

So we store the children that we have suspended on that object.

And now we can access that object in our actual server logic.

So after we've rendered all of our component tree, we just check if there's any, suspended components, in our object.

And if there's, we basically wanna load them and then replace the fallback with our suspend, with the actual content.

So for the actual resolving and, rendering, we can do what we've done before in the, in the classical server side rendering.

So we basically get all the suspend components, the object entries, Gives us the ID and the value, which is the children.

So we can loop over that.

And then we just, actually we want to.

We then get the element like we did before by just awaiting, the children.

Actually children can be an array or just a single element, so let's make sure that it's actually an array.

Then we have an nested loop somewhere.

Yeah, definitely should have prepared this before.

I downloaded cursor for this in the hope that it helps me, but it didn't.

So we, know we have our children and then we want to basically loop through our children.

So that's actually the contents.

All children map and then the element is like the child.

So that's what we had before in the classical server side rendering, right?

This can go and.

Cool.

Now that we dealt with it, we can also delete, the entry from the suspended object and that should be rest, right?

All.

That was painful.

But, so basically just to, to recap what we're doing here is after we've rendered our whole tree, we check if we have any suspended components based on our suspense logic.

And if we have, then we run through those as if we rendered them classically and just depend them to the to the stream.

So if we refresh the page, now it's broken because I didn't import suspend it.

And it's now popping in.

You can see there's a gap that's still the loading state that is not visible, but it's there.

It's highly visible.

So basically we're very close now, so we have a way to deal with suspended components and we use the stream to once they're available they pop into existence, but we don't really have a logic to replace the fallback UI yet.

How do we do that?

Because by definition, like I said, the stream can only append to the document.

So how do we actually replace an element that's been rendered before with a new element that's just come in?

In short, we can't, that's a bummer.

But like most things, the real magic happens with JavaScript.

So like we do need a little bit of client side JavaScript to actually make this work.

And for that we need a few things.

Like first we need a way to actually identify the fallback.

In the DOM.

So we wrap this in a div and give it a data suspense id.

This way we can find the diff later.

We don't want this diff to mess with the layout so we can just give it a display of contents.

Free CSS tip on the site.

Display contents basically tells the CSS renderer to ignore this for any layout purposes.

So if this is rendered within a flex layout or a grid layout, it'll not mess with that.

It will just look in the children, to do its grid or layout, or flex things, which is pretty neat.

And now that we can identify the fallback we want to do two things.

First, we wanna wrap the actual rendering of the, actual content in a template tag.

Again, if you've seen the talk from Scott this morning, that will look somewhat familiar.

All that does is telling the browser, ignore it, like the browser.

It will still be in our, in our response, but the browser will just not render it.

And now we need a way to actually swap out this new, or swap out the fallback AI with that new content.

So let for that there, there's a million ways to do that.

I should have prefaced that in the beginning of the talk.

This is not exactly how React does it.

Please don't expect it.

This is whole, like I'm trying to explain suspense conceptually.

How I'm gonna do it is with, custom elements, just because I think they're interesting to play with and it's an easy way to do it.

It's not how React does it another, but it does it very similarly.

So if we just pretend we have a custom element called suspense content.

And let's call it target id.

So basically all we're doing here is rendering this custom, element and giving it a target id.

So again, we can actually match the real content with the fallback content.

And then we need that little bit of JavaScript.

So if we detected that there's any, any suspended components, we create this suspended content, custom element.

And for that, I'm gonna cheat for the sake of time, and I'm just gonna copy it from my notes.

I am still gonna explain it.

So what we're doing here, is essentially, we create that, window custom element called suspense content.

And again, like in the talk this morning, custom elements have that nice thing that's, those lifecycle methods.

The one that we are using is connected callback.

It's gonna be called whenever this custom tag is rendered anywhere in our application.

And in here, all we really do is we look at the previous sibling, which we know because we're controlling the rendering, we know is gonna be the template tag, and we get the ID from the targetID.

Then we use that ID to find the data suspense ID element, which is the loading state.

And then we're just swapping out the HTML for that.

So in the hope that all of that actually works, can refresh the page.

And once the continents is there, it basically replaces the loading state now.

So this is exactly what we want, right?

So we have a loading state, and through the streaming we get the new content in.

And the out of order streaming part is that we can actually replace previously rendered content.

And we're not limited to single, suspense boundary either.

So like on the client, we can just throw a bunch of them in here.

Basically for each section we're gonna do one.

And if we refresh the page, again, you can't really see that well, but like now every section has its own suspense boundary, its own loading state, and they can load in immediately when they're ready.

They don't have to wait for all three sections to be ready.

So that's even better.

And that's the magic of suspense, both on the client, on the server.

You have that granular control over your loading state.

And now if we click on any link, the feedback is immediate.

We have a nice loading animation and all of that.

This is all pretty cool.

I hope that kind of somewhat explained the magic behind that out of order streaming and what suspense does on the server.

I, let's go back here.

That works.

These are some resources, that I think are really good, to just get started with that topic, read up on the official docs, on the client side, suspense stuff.

There's a link to the RFC for suspense on the server explaining that whole concept of what they're trying to achieve and how they're, achieving it.

There's some YouTube and, blog posts that also really help me.

These are really good starting points, and then you can dive deeper into the topics that you find interesting.

This is a QR code to the GitHub repo.

With the code, that we've just, the demo code that we've just gone through, a link to the slides and to my socials.

I'm not very active on socials, but here we are.

And that's pretty much all from me.

I think we have plenty of time for questions.

screencast of a loading page.
Diagram illustrating server and client technologies with boxes labeled 'HTML & PHP', 'JQUERY & AJAX', 'REACT SSR & SSG', and 'REACT SPA'. Arrows indicate flow between 'JQUERY & AJAX' to 'REACT SPA' and back from 'REACT SPA' to 'REACT SSR & SSG'.

SERVER

  • HTML & PHP
  • React Server Components
  • React SSR & SSG

CLIENT

  • jQuery & AJAX
  • React SPA

Hydration is slow

Diagram illustrating the relationship between server and client technologies: Server includes "HTML & PHP," "React Server Components," and "React SSR & SSG." Client includes "jQuery & AJAX" and "React SPA," with an arrow noting slow hydration.

WHY SUSPENSE IS GREAT

WHY SUSPENSE IS GREAT

SERVER-SIDE RENDERING
INTRODUCE STREAMING
OUT-OF-ORDER STREAMING
Three-panel illustration: First panel shows a neutral cartoon character with thumbs up, labeled 'Server-side rendering'. Second panel shows the same character with a double thumbs up, labeled 'Introduce streaming'. Third panel shows the character with an excited expression and stars around their eyes, labeled 'Out-of-order streaming'.

BUILDING SUSPENSE

LET’S WRITE SOME CODE
Julian describes the process of building the suspense-like features. The live coding was very difficult to capture screen shots of in a meaningful way.
import { fetchTitle } from "../api/title.ts";
import { Image } from "./Image.tsx";

export async function Title({ id }: { id: string }) {
  const data = await fetchTitle({ id });
  return (
    <section className="flex flex-row gap-2 items-start">
      <Image src={data.image} className="w-[150px] aspect-[10/16] rounded-sm" />
      <div className="flex flex-col p-2 gap-2 flex-1">
        <h2>{data.name}</h2>
        <p className="opacity-50 text-xs">
          {data.datePublished.substring(0, 4) + data.genre.join(", ")}
        </p>
        <p
          dangerouslySetInnerHTML={{
            __html: data.description?.replaceAll("-", "—"),
          }}
        />
      </div>
    </section>
  );
}
Thu 28 Nov 2:00 pm
Screenshot of a code editor showing TypeScript code for an asynchronous function titled 'Title'. The function imports modules, fetches data, and returns a JSX section with image and text details.
import { fetchDetails } from "../api/details.ts";
import { Image } from "./Image.tsx";

export async function Details({ id }: { id: string }) {
  const data = await fetchDetails({ id });
  return (
    

Cast

<div className="flex flex-row gap-2 overflow-auto"> {data.cast.edges.map((edge: any) => ( <div key=edge.node?.name?.id className="flex flex-col content-start justify-start shrink-0 w-[100px] overflow-hidden grayscale" > Image src=edge.node?.name?.primaryImage?.url alt=edge.node?.name?.nameText?.text className="h-[100px] w-[100px] object-cover rounded-sm" > </div> <div className="p-1"> {edge.node?.name?.nameText?.text} </div>
); }
Screenshot of a code editor showing a React functional component definition. The component uses data fetched from an API to render a list of images and text related to cast members.
app.use(express.static("out"));

app.get<{}, any, any, { id: string }>("/", async (req, res) => {
  const app = (
    <>
      <Header />
      <Title id={req.query.id} />
      <Details id={req.query.id} />
      <Similar id={req.query.id} />
    </>
  );
});

let content = '<!doctype html><html><head><meta charset="UTF-8"><link href="./index.css" rel="stylesheet"></head><body>';
const contents = await Promise.all(
  app.props.children.map(async (child: any) => {
    const element = await child.type(child.props);
    return renderToString(element);
  })
);
content += contents.join("");
Screenshot of a code editor displaying TypeScript code for an Express application with JSX components for a web page structure.
res.setHeader("Content-Type", "text/html");
res.write(
  `<!doctype html><html><head><meta charset="UTF-8"><link href="./index.css" rel="stylesheet"></head><body>`
);

await Promise.all(
  app.props.children.map(async (child: any) => {
    const element = await child.type(child.props);
    res.write(renderToString(element));
  })
);

res.write(`</body></html>`);
res.end();

app.listen(port, () => console.log(`Listening on http://localhost:${port}`));
res.setHeader("Content-Type", "text/html");
res.write(
  `<!doctype html><html><head><meta charset="UTF-8"><link href="./index.css" rel="stylesheet"></head><body>`
);
for (let child of app.props.children) {
  app.props.children.map(async (child: any) => {
    const element = await child.type(child.props);
    res.write(renderToString(element));
  });
}
res.write(`</body></html>`);
res.end();
app.listen(port, () => console.log(`Listening on http://localhost:${port}`));
Screenshot of a text editor displaying TypeScript code within a file named index.tsx in a project structure, focusing on async child element rendering.
const app = express();
const port = process.env.PORT || 3000;

app.use(express.static("out"));

app.get("/", any, any, { id: string }>(req, res) => {
   const app = [
      <Header />
      <Suspense fallback={} >
         <Title id={req.query.id} />
         <Details id={req.query.id} />
         <Similar id={req.query.id} />
      </Suspense>
   ];
});

res.setHeader("Content-Type", "text/html");
res.write(`<!doctype html><html><head><meta charset="UTF-8"><link href= "./index.css " rel="stylesheet"></head><body>
Screenshot of a code editor with JavaScript code defining an Express application, showing routes and components using Suspense and dynamic queries. Menus and tabs are visible.
import { Suspense } from "./components/Suspense.tsx";

const app = express();
const port = process.env.PORT || 3000;

app.use(express.static("out"));

app.get<{}, any, any, { id: string }>("/", async (req, res) => {
    const app = (
        <Header />
        <Suspense fallback={<TitleSkeleton />}>
            <Title id={req.query.id} />
            <Details id={req.query.id} />
            <Similar id={req.query.id} />
        </Suspense>
    );
});

res.setHeader("Content-Type", "text/html");
res.write(
    `<!doctype html><html><head><meta charset="UTF-8"><link href="./index.css" rel="stylesheet">
Screenshot of a code editor with open tabs displaying JavaScript/TypeScript code related to a web application setup using Express and React components.
res.setHeader("Content-Type", "text/html");
res.write(
  '<doctype html><html><head><meta charset="UTF-8"><link href="./index.css" rel="stylesheet"></head><body>'
);
for (let child of app.props.children) {
  const element = await child.type(child.props);
  res.write(renderToString(element));
}
if (Object.keys(suspended).length > 0) {
  const contents = await Promise.all(
    Object.entries(suspended).map(async ([id, content]) => {
      const children = Array.isArray(content) ? content : [content];
      const element = await children.type(children.props);
      return renderToString(element);
    })
  );
  res.write(contents.join(""));
}
res.write('</body></html>');
Screenshot of a code editor displaying TypeScript code that sets HTTP headers and renders React components as HTML strings, with a file directory on the left and editor tabs open at the top.
for (let child of app.props.children) {
    const element = await child.type(child.props);
    res.write(renderToString(element));
}

if (Object.keys(suspended).length > 0) {
    await Promise.all(
        Object.entries(suspended).map(async (id, content) => {
            const children = Array.isArray(content) ? content : [content];
            const contents = await Promise.all(
                children.map(async (child) => {
                    const element = await child.type(child.props);
                    return renderToString(element);
                })
            );
            res.write(contents.join(""));
            delete suspended[id];
        })
    );
}

res.write(</body></html>);
res.end();
Screenshot of code editor showing TypeScript code for handling promises and rendering components within an application.
export let suspended: any = {};

let uuid = 0;
export function Suspense({ children, fallback }: any) {
    const id = uuid++;
    suspended[id] = children;
    return <div data-suspense-id={id}>{fallback}</div>;
}
Screenshot of a code editor with the file "Suspense.tsx" open, showing a TypeScript component for suspending components with a fallback option.
Screenshot of a code editor displaying TypeScript code for handling suspended components and rendering them to a string. The editor shows a file tree on the left and the open file `index.tsx` with highlighted syntax.
app.get("/", (req, res) => {
  if (Object.keys(suspended).length > 0) {
    res.write(
      <script>
        window.customElements.define('suspense-content',
          class SuspenseContent extends HTMLElement {
            connectedCallback () {
              const content = this.previousElementSibling.content;
              const id = this.getAttribute('target-id');
              const target = document.querySelector('[data-suspense-id="' + id + '"]');
              
              target.innerHTML = '';
              while (content.firstChild) {
                target.appendChild(content.firstChild);
              }
            }
          }
        );
      </script>
    );
  }
});

await Promise.all(
  Object.entries(suspended).map(async ([id, content]) => {
    const children = Array.isArray(content) ? content : [content];
//
Screenshot of a code editor showing a code file named index.tsx. The file appears to be defining a custom HTML element using JavaScript. The editor has a sidebar with a file tree visible.

Good Resources to Get Started

https://react.dev/reference/react/suspense
Official Docs — Suspense (on the client)

https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md
RFC Document for Suspense on the Server

https://www.youtube.com/watch?v=mAebeQhZR84
Ben Holmes — React Server Components from Scratch

https://gal.hagever.com/posts/out-of-order-streaming-from-scratch
Gal Schlezinger — Out of Order Streaming from Scratch
Thank you!
https://github.com/julianburr/talk-lets-build-suspense
https://www.julianburr.de/developer-summit-2024-slides.pdf
https://linkedin.com/in/julianburr
https://github.com/julianburr
QR code with a small dinosaur in the center and a spider descending from the top.
  • asynchronous
  • Ajax
  • server side rendering
  • hydration
  • virtual DOM
  • event listener
  • fetch
  • asynchronous
  • write method
  • loading state
  • append
  • display: contents
  • custom elements