question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Svelte suspense (request for comments)

See original GitHub issue

After reading this issue, I came up with a Suspense component for Svelte, replicating the behaviour of React Suspense. No React Cache, no throwing promises, no modifying your component to fit a use case, just Svelte component composition. A demo is available in the corresponding GitHub repository. Note that I could not have the demo running in Svelte REPL due to some issues with loading the axios package.

svelte suspense demo

The behaviour of the Suspense component is implemented with the Kingly state machine library. The summary of ‘findings’ can be found here. For info, here is the underlying state machine specifying the suspense behaviour:

suspense machine

The demo showcases the API and I will quickly illustrate it here. The demo consists of loading a gallery of images. The suspense functionality is applied twice: when fetching the remote data containing the image URLs, and then for each image which is subsequently downloaded. While the remote data is fetched, a spinner will display if fetching takes more than a configurable time. Similarly, images placeholder will also display a spinner if downloading the image takes more than a configurable time.

Firstly, the suspense functionality for the remote data fetching is implemented as follows:

<script>
 ... a bunch of imports

  const iTunesUrl = `https://itunes.apple.com/in/rss/topalbums/limit=100/json`;

  function fetchAlbums(intents){
      const {done, failed} = intents;
      axios.get(iTunesUrl)
           .then(res => res.data.feed.entry)
           .then(done)
           .catch(failed)
    }

</script>

<div class="app">
    <Header />
    <div class="albums">
        <Suspense task={fetchAlbums} let:data={albums} timeout=10>
            <div slot="fallback" class="album-img">
                <img alt="loading" src="https://media.giphy.com/media/y1ZBcOGOOtlpC/200.gif" />
            </div>
            <div slot="error" class="album-img">
              <h1>ERROR!</h1>
            </div>
            <LazyLoadContainer>
                {#if albums}
                  {#each albums as album, i}
                  <LazyLoad id="{i}">
                      <Album {album} />
                  </LazyLoad >
                  {/each}
                {/if }
            </LazyLoadContainer>
        </Suspense>
    </div>
</div>

Note that the fetch task and minimum time (timeout) before displaying the spinner is passed as parameters of the Suspense component, while the fetched data is exposed to the slot component through the data property. Note also how the fetching function is passed the done and failed callback to signal successful completion or error of the remote fetching.

The fallback slot is displayed when the timeout is expired. The error slot is displayed when fetching the data encounters an error.

Secondly, the Album component suspense functionality is implemented as follows:

<ul class="album">
    <li class="album-item">
        <Suspense let:intents={{done, failed}} timeout=0>
            <div slot="fallback" class="album-img">
                <img alt="loading" src="https://media.giphy.com/media/y1ZBcOGOOtlpC/200.gif" />
            </div>
            <a href={link} target="blank" class="link">
                <img class="album-img"
                     on:load={done}
                     src={image}
                     alt={'itunes' + Math.random()} />
            </a>
        </Suspense>
    </li>
    <li class="title album-item">
        <a href={link} target="blank" class="link">
            {title.slice(0, 20)}..</a></li>
    <li class="price album-item">Price:{price}</li>
    <li class="date album-item">Released:{formatDate(date, "MMM Do YY")}</li>
</ul>

This time the Suspense component passes done and failed callbacks to its children slots. When the image is loaded, the done callback is run.

This works well and I believe the API separates well the suspense functionality or concern from the slots. What we basically have is parent and children components communicating through events, except that the event comes included in the callback. As the demo shows, there is also no issues with nesting Suspense components.

This GitHub issues has two purposes:

  • gettign feedback on the API
  • giving feedback on Svelte slot composition

The first point is more about hearing from you guys.

About the second point:

  • slot composition is a powerful and flexible mechanism, specially in conjunction with scoped slots
  • however, a few things would be nice:
    1. being able to operate on the slot as if it were a regular html element. This mean the ability to style a slot with classes or possibly other attributes (<slot class='...' style='...'> </slot>). Add some extra attributed to cover generic needs, i.e. needs that are independent of the content of the slot. To implement the suspense functionality I had to resort to hide the default slot with display:none. Unfortunately to do that I had to wrap the slot around a div element, which can have side effects depending on the surrounding css. A syntax like <slot show={show}> </slot> would have been ideal. After thinking some more, I think that slot cannot be considered as regular HTML elements but as an abstract container for such elements. The operations allowed on slots should be operation on the container, not on the elements directly. Styling or adding classes an abstract container does not carry an obvious meaning, as the container is not a DOM abstraction. The current operations I see existing on the container are get (used internally by Svelte to get the slot content), show could be another one. The idea is that if you have a Container type, and a Element type, your container is C<E>. If you do operations that are independents of E, you can do only a few things like use E (get), ignore E (don’t use the slot), repeat E (not sure how useful that would be), conditionally use E (show, of type Maybe<E>). Using any knowledge about the E I think leads to crossing abstraction boundaries which might not be a good thing future-wise.
    2. having slots on component just like if components were regular elements
    3. having dynamic slots. In the Suspense component, I use if/then/else to pick up the slot to display, which works fine (see code below). It would be nice however to have <slot name={expression ...}>:
{#if stillLoading }
  <slot name="fallback" dispatch={next} intents={intents} ></slot>
{:else if errorOccurred }
  <slot name="error" dispatch={next} intents={intents} data={data}></slot>
{:else if done }
  <slot dispatch={next} intents={intents} data={data}></slot>
{/if}
<div class="incognito">
  <slot dispatch={next} intents={intents} ></slot>
 </div>

I am not really strong about the dynamic slots. It might add some complexity that may be best avoided for now. The first and second point however I believe are important for abstraction and composition purposes. My idea is to use Svelte components which only implement behaviour and delegate UI to their children slots (similar to Vue renderless components). Done well, with this technique you end up with logic in logic components, and the view in stateless ui elements.

The technique has additionally important testing benefits (the long read is here).

For instance the behaviour of the Suspense state machine can be tested independently of Svelte - and the browser, and with using a state machine, tests can even be automatically generated (finishing that up at the moment). Last, the state machine library can compile itself away just like Svelte does 😃 (the implementation is actually using the compiled machine).

About testing stateless components, Storybook can be set to good purpose. What do you Svelte experts and non experts think? I am pretty new with Svelte by the way, so if there is any ways to do what I do better, also please let me know.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:24
  • Comments:27 (1 by maintainers)

github_iconTop GitHub Comments

11reactions
ryansolidcommented, Jul 30, 2019

Perhaps my misunderstanding, but I understand the key benefit of Suspense to be the inversion of control (children control the loading/entering the suspense state). It isn’t difficult today to conditionally render child content based on whether its loaded, to show previous state, or a loading indicator on a cancelable timer. You can always push that to the parent to achieve this effect. What makes Suspense interesting is the parent goes “Here’s a placeholder. Children do whatever you need to do, I don’t even need to know what exactly. I’m here to support you and make sure you don’t embarrass yourselves.” We can probably avoid throwing Promises, but I think we need to at minimum execute the children without showing them if necessary. This makes everything more complicated like conditional rendering in between the Suspense placeholder and the asynchronous call. But that’s the problem we need to solve.Thats what makes this interesting.

3reactions
ryansolidcommented, Mar 12, 2021

Yeah a lot has changed in understanding (and even the React implementation) since the original post was made. I think things have been better defined now and Svelte should be able to better make a decision where to go here.

Vue/Preact decided to not follow React all the way down the concurrent mode rabbit hole. In fact, what originally was considered a single thing is now arguably a couple different but related features.


  1. Suspense the placeholder. This is what those libraries implemented and more or less is what @ADantes is illustrating. In the case of a reactive library using context makes a ton of sense (as really the only way to connect the dots when we aren’t doing full top down re-rendering) and one could just implement that and call it a day. This is all Vue or Preact is doing. One could implement this and get a similar experience.

  1. Transitions. This is where the conversation went off a bit. Suspense the placeholder is great for initial loads but some of those early demos showed other behaviors without really defining them. Transitions in React are where a change set is isolated and not committed until all promises that are initiated due to that change that are read under a Suspense boundary completely resolve. Ie… if someone transitions to the next tab the actual state change of the tab change is held from the outside view until all the data is loaded for the new tab and we can update everything at the same time.

The idea here is that in order to maintain async consistency we can’t let an in flight change be visible outside the scope of that change. A more concrete example might be picturing a “Like” button on a User information carousel, where you click next to load the next user. In the ideal world proposed here, one could click next starting the rendering and loading of the next user off screen, and then the enduser can click like button while this is happening and still have the current in view user record be the one that is liked as the updated user id won’t have propagated yet.

Basically both rendering the next possible state (to perform data loads), while showing the current state that is completely interactive without causing any inconsistencies both in data or in visual tearing. Ie… the page number doesn’t update until everything is completed. This is the benefit of transitions.

Now React has refined this further stating that even under transitions it is only previously rendered Suspense boundaries that hold, and newly created ones instead fall to the fallback state (ie any async reads under them are not counted towards the transition). In so the enduser has the ability to create the boundaries and nested boundaries of where you want to hold or show placeholders while it frees up things like routers or design systems to just blindly implement transitions into their controls. If those transitions never trigger something async that is read under a Suspense boundary they are basically identity functions that do nothing. However if the application implements Suspense they tap into a much richer system for controlled orchestration of both placeholders and held(stale) states.

This seems complicated and it is, but the introduction of stale states save from excessive falling back to loading placeholders. You can picture with tab navigation if you are already looking at a tab and click the next one you don’t need to replace the whole view with a placeholder… a simple indicator of the stale state would be much less jarring. Browsers natively do this on page navigation. Even if a page is slow to load if it were server-rendered (ie the HTML all present when it does load) it skips ever showing the white screen. You see the current page, then some sort of loading indicator and then bam the new page. We are less accustomed to this these days because of all the client-side rendering going on and forced loading states. But the native browser handles this pretty seamlessly.


  1. Ok so clearly we can implement number 1 and call it a day like Vue or Preact. But the other discussion was interesting to see if it could be modeled in Svelte. It is probably unnecessary. But React had good reason to go this way as this same concurrent rendering in their case attached scheduling is what is going to be powering their HTML streaming and Async server rendering that is in their upcoming Server Components. Now I’ve talked with a few authors of other libraries and they are positive (as am I) that this is possible without concurrent rendering. Mostly that the scheduling piece can be viewed as a 3rd feature. We talked a bit about it above but that’s the part that others have eluded to not really being necessary in a performant system like Svelte. If you aren’t scheduling huge amounts of diffing work I mean why add this overhead, it’s a solution asking for a problem.

So I know reactive frameworks like Svelte (and I’ve made the same wager with the reactive frameworks I am maintaining) don’t need transitions to get this sort of rendering. I already have it working in both of mine without (even though I have implemented transitions in one and intend to in the other). So the 3rd feature I think is unneed here.


Analysis:

If desirable it’s probably fine to go ahead with 1 and forget about it. Mind you it is a lot less interesting for a library that already has an await control flow. Like just hoist the await, pretty easy way to get the placeholder in the right spot. Like if you not concerned with trying to mimic the transition behavior of stale states, you don’t need to worry about preserving anything. Vue and Preact added this Suspense component because they didn’t have await.

Suspense is a bit slicker to be sure but most of that comes in actually burying the promise in a resource primitive when it comes to data loading. You can basically treat it like a typical Svelte store and not even write the clearly async await blocks in the child code. A subscription to that store under a Suspense boundary is sufficient and child components can trigger Suspense without even being the wiser they are dealing with async data. I mean it’s powerful but is it worth adding a new primitive type… a new sort of Svelte store if you may. It does have to be one since it needs to be able to persist cross component boundaries. This is a lot for only a portion of the story, a portion already mostly taken care of by Svelte.

The second feature is mind warping and really what people have been referring to when talking to this in the past. Rich in an issue a while back was talking about how to encode change sets as a way to achieve this. I’ve been down that road too but ultimately decided that forking the reactive primitives into transactions(while holding updating existing side effects) is probably the best way to achieve this in a reactive system. But let’s face it, again this sort of quantum reactivity is a steep price for basically a navigation trick. It’s a freaking cool navigation trick but you have to ensure large parts of your system are side-effect-free. This is all the grumbling you hear about the concurrent mode in React.

There are other challenges being a compiled language. It’s doable with dedicated reactive primitives but for a language it is a bit trickier. You could reserve syntax for it. Like $: with await’s in it could basically create the new primitives under the hood but would you really want to introduce the equivalent of the useTransition API. I mean you need it if you want this behavior to be opt in and I personally feel it needs to be opt-in as being on by default is nonsensical for end users since you’d in some cases always need to show them data in the past even after they thought they updated it. I will leave it up to the minds here to decide whether that is suitable for Svelte. But I suspect this whole thing brings with it a sort of weight that might be best left on the table. Not to say it isn’t beneficial, but does it align with Svelte’s goals?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Suspense in Svelte: Writing Components That Don't Care
Suspense in Svelte: Writing Components That Don't Care ... Loading data, managing async requests, and communicating status information back to the ...
Read more >
Suspense in Svelte : r/sveltejs - Reddit
It would require `if`s everywhere to *not* use the real `album` which is so noisy. So yeah, I see the point now, it's...
Read more >
Svelte Suspense — Kingly.js
Svelte Suspense. Motivation. This example illustrates a simple hierarchical state machine implementing a behavior similar to React Suspense.
Read more >
Suspense • REPL • Svelte
<script> // This is a demo showcasing simple data loading // with @jamcart/suspense. Since this is about // loading states, you may want...
Read more >
дэн on Twitter: "to give you a concrete example, consider an ...
New Suspense SSR Architecture in React 18 · Discussion #37 · reactwg/react-18 ... RFC: React Server Module Conventions by sebmarkbage · Pull ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Hashnode Post

No results found