-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Async data #654
Comments
How I handle it in a large project is I generate the state that the route uses on the server (from redis, MySQL, other APIs, etc) then pass it as JSON through a script element. That ensures that both SSR and the client are receiving the same state. |
That makes sense for the initial load — how do you handle client-side route changes? Not sure I got the API quite right above, I don't think there's a great way to instantiate the component if it's set up like that, we probably don't want BlogPost.preload({ slug: 'components-with-async-data' }).then(post => {
new BlogPost({
target,
data: { post }
});
});
// generalised
Promise.resolve(Component.preload ? Component.preload(params) : {}).then(data => {
new Component({ target, data });
});
We might actually need to solve this problem, now that I think about it. For example, the component that defines the Any good examples of how other frameworks have solved this? |
Honestly, is there anything preventing us from using redux + redux thunk? Seems to handle that case quite nicely. In theory if #469 ever gets implemented we could have svelte-redux or something to have first class support for redux as a library. |
What would that look like in practice (i.e. from the point of view of writing the code that lives inside your route handlers on both server and client)? I'd be wary of hitching our wagon to a specific state management tool, that's a holy war I don't really want to pick a side in! Besides, it seems that the hardest problem here isn't implementation so much as design. |
What this issue seems to be is a way for a component to load data before it's mounted. A clean way to solve that is #469 but the current way to solve it is to use any state management library + just a if check if the data is loaded. |
#469 is purely about element-level lifecycle hooks, do you mean a different issue? By 'if check' do you mean in the template, as in |
I might be confused what this is suggesting. Isn't it passing data to a component to load before the component is mounted? |
At a high level it's just saying that there needs to be an idiomatic way within the component definition to express asynchronous data dependencies, that applies in both server- and client-side contexts |
Is this just as simple as checking if data returns a promise? If so wait for it to resolve before mounting? Perhaps combine that with a mountRequiresData boolean so the user can return a promise for async data but still have it mount if desired? |
In my mind this is very strongly something that should be implemented with dynamic components (#640). As long as the dynamic component API is expressive enough, people can pick from a few general-case async-loading components, or make one that fits their specific case. |
@TehShrike was that a response to this comment on Gitter? I think it's a different problem to that of async data. I should make a separate issue for |
Oh, sure, that's another case that I think should be handled entirely by dynamic components instead of adding new syntax. But no, I really do think that async loading of data is also definitely something that should be achievable/achieved with dynamic components. |
Just an idea, can {{#pending foo}}
Loading foo...
{{#then foo}}
Foo has a value of {{foo}}
{{#catch foo}}
No foo this time
{{/foo}}
<script>
export default {
data () {
return {
foo: () => fetch(url).then(d => d.json())
};
}
};
</script> and then call it like <ComponentThatLoadsStuff url="//foo.com?post=123"/> |
@aubergene Dude! I think you're onto something here. Elsewhere in Svelte (and Handlebars generally), {{#pending foo}}
Loading foo...
{{then value}}
Foo has a value of {{value}}
{{catch err}}
No foo this time (because {{err.message}})
{{/pending}} ...but I think the idea of solving this problem with syntax is brilliant. Maybe Could also have a shorthand like this, if you didn't care about pending/error states: {{#await foo then value}}
Foo has a value of {{value}}
{{/await}} One thing to think about: often, you might want to preserve the previous value until the new one has resolved, for example a list of autocomplete suggestions (you don't want them to disappear each time you type a character): <input list='suggestions' bind:search>
{{#await suggestions}}
<span>loading...</span>
{{then value}}
<datalist id='suggestions'>
{{#each value as suggestion}}
<option>{{suggestion}}</option>
{{/each}}
</datalist>
{{catch err}}
<span class='error'>could not get suggestions!</span>
{{/await}}
<script>
export default {
computed: {
suggestions: search => fetch(`/suggestions.json?q=${search}`).then(r => r.json())
}
};
</script> How might we express that idea — that we want the 'loading...' text initially, but thereafter we should preserve the resolved value until another one resolves? |
One other thing to ponder: how would this work with SSR? If we had an async SSR renderer it could stream HTML to the client (i.e. it sends what it has until it hits the Then, when the client-side JavaScript kicks in without a promise representing the data, it would presumably have no choice but to display the pending state when it hydrated the markup. I'm not sure what a good solution to that would be. Probably getting ahead of ourselves, just something to be mindful of. |
One possible solution: if the awaited value isn't a promise (i.e. isn't 'thenable'), skip to the |
Reminds me of this excellent post on different UI states: http://scotthurff.com/posts/why-your-user-interface-is-awkward-youre-ignoring-the-ui-stack First-class promise support would make handling three of those states way easier. I would prefer the words used being
There'd be stuff to talk about (If there's no |
Good read, thanks. Yeah, 'first-class' is a good phrase to use here. Interesting. I always use 'fulfil' rather than 'resolve' in my Promise callbacks — I always considered resolution as a term that described either fulfilment or rejection (also, it's less easy to trip up when you tab-complete from For that reason I think To me it makes sense to think about the loading state first, then you think about what to do with the eventual value (or error). The developer's chronology matches the user's. I think an
Good question. I think probably yes (whether it's Plenty of room to bikeshed that stuff anyway! I've been thinking about this question...
...and I think I have an answer that I like — {{#await once suggestions}}
<span>loading...</span>
{{then value}}
<datalist id='suggestions'>
{{#each value as suggestion}}
<option>{{suggestion}}</option>
{{/each}}
</datalist>
{{catch err}}
<span class='error'>could not get suggestions!</span>
{{/await}} In that case, we could add a second argument to the {{#await once suggestions}}
<span>loading...</span>
{{then value, pending}}
<datalist id='suggestions' style='opacity: {{pending ? 0.5 : 1}}'>
{{#each value as suggestion}}
<option>{{suggestion}}</option>
{{/each}}
</datalist>
{{#if pending}}
<span>updating...</span>
{{/if}}
{{catch err}}
<span class='error'>could not get suggestions!</span>
{{/await}} Feels that would solve a lot of fairly common (but non-trivial) UI problems that would normally have you reaching for RxJS. |
I love the idea of the templating language being aware of promise state, but as soon as it's juggling old versions of state and new versions of state with the same name, my mental model gets sticky. What happens to set({ foo: startAsyncJob('A') })
set({ foo: startAsyncJob('B') })
set({ foo: startAsyncJob('C') }) We'll say that first, job C resolves, then A rejects, and then later B resolves. If I understand the But if I understand the For my own sanity, if I had a view that actually allowed for those sorts of launching-async-jobs-arbitrarily flows, I'd do it in some sort of state-management safe zone outside of Svelte anyway. I'm a bit sleepy so that may not all make sense :-x feel free to question my statements here. At the very least, this seems like the sort of thing that could be added later if that remained a pain point for folks using whatever the initial await/then/catch syntax is. |
That makes sense, but I've found that in my code, I end up caring less about the order of the code inside my if/else blocks, and more about the conditions being clear/obvious (e.g. avoiding double negatives like So my instinct is to just push for each |
Glad you like the idea. I actually expected that Svelte already worked like this, seemed fairly intuitive and so I was rummaging around wondering why passing a promise wasn't working for me. I like your I'm not sure about the I'm not the greatest expert on promises but couldn't something like this be achieved in the JS with a helper which wraps the initial promise and you can pass in a new one but it returns the result of the current one until the new one resolves? That seems like a more generally helpful thing. |
Fair enough, maybe <input list='suggestions' bind:search>
{{#if error}}
<span class='error'>could not get suggestions!</span>
{{elseif suggestions}}
<datalist id='suggestions'>
{{#each value as suggestion}}
<option>{{suggestion}}</option>
{{/each}}
</datalist>
{{else}}
<span>loading...</span>
{{/if}}
<script>
export default {
oncreate() {
let token;
this.observe('search', search => {
const currentToken = token = {};
fetch(`/suggestions.json?q=${search}`)
.then(r => r.json())
.then(suggestions => {
if (currentToken !== token) return; // responses arrived out of order
this.set({ suggestions, error: null });
}, err => {
this.set({ error, suggestions: null });
});
});
}
};
</script>
In that case, you would go straight from awaiting to resolved with C. A and B would be discarded, because as of the third line they're no longer valid values for
Yep! |
The more I think about it, the more I like your earlier suggestion, including the |
I make the @aubergene's words mine. I like the |
I don't say this lightly. +1 😂 |
{{#await thePromise}}
<p>loading...</p>
{{then theData}}
<p>got data: {{theData.value}}</p>
{{catch theError}}
<p>whoops! {{theError.message}}</p>
{{/await}} |
This is AMAZING. I'm not sure I know of anything like this in any other framework I've used. |
@arxpoetica RactiveJS, but we've stolen it from here ))))) |
@Rich-Harris Is it currently possible for the server renderer to try to resolve the promise during SSR so that the client can receive the HTML already filled with the data from the promise? Like what |
@jackyef we need this! |
@Rich-Harris what happened to the I'm stuck now wondering how to keep the data of the previous promise without moving on to the loading state again. |
It lives on in the also-old issue #955. |
Been refactoring https://svelte.technology — inching towards a more declarative/universal way of handling routing etc.
One thing that stands out is that there's really no good way for components to declare their own data dependencies in a universal way. So you have to have a dedicated handler per route, in case you need to do something like fetch a blog post's JSON.
It would be nice if that stuff could sit in the component itself. You'd still have to handle data fetching differently between server and client, but at least you'd be able to declare all your routes declaratively (up to a point, anyway).
Maybe something like this:
This only really works for top-level components — it could get pretty crazy if you had something like this:
Thoughts?
The text was updated successfully, but these errors were encountered: