Description
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.
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:
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:
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 (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<slot class='...' style='...'> </slot>
).display:none
. Unfortunately to do that I had to wrap the slot around adiv
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 areget
(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 isC<E>
. If you do operations that are independents ofE
, 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). Using any knowledge about theE
I think leads to crossing abstraction boundaries which might not be a good thing future-wise.- having slots on component just like if components were regular elements
- having dynamic slots. In the
Suspense
component, I useif/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.