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

Colocate remote data fetching in svelte component files #4741

Closed
brucou opened this issue Apr 28, 2020 · 10 comments
Closed

Colocate remote data fetching in svelte component files #4741

brucou opened this issue Apr 28, 2020 · 10 comments
Labels
awaiting submitter needs a reproduction, or clarification temp-stale

Comments

@brucou
Copy link

brucou commented Apr 28, 2020

Is your feature request related to a problem? Please describe.
I just came upon this idea, and apologies if by any chance it has already been discussed before. This occurred to me while watching a React talk dealing with all the stuff that they put in place to ensure a smooth user experience, including loading experience. There is a ton of complexity there, and I wonder if a compiler-based approach may not help.

Describe the solution you'd like
The idea is, like React/Relay folks do, to colocate the description of the data a component needs in the component template. So for instance, in a Post.svelte component:

<Title>{data.title}</Title>
<Body>{data.body}</Body>

<style> ... </style>
<script> ... </script>
<data> ... </data>

In the data section (name could be anything, that is just the first thing coming to mind), devs would put a description of the remote data used by the component, a-la graphQL:

fragment Post_fragment on Post {
  title
  body
}

In a React context, GraphQL clients like Relay gather all the data requirements from components into a single query, and minimize server roundtrips. The alternative is to render then fetch, which I believe is the common approach used in a Svelte context (Sapper use excluded). That approach may lead to this kind of loading experience:

Loading experience made of an increasing number of loading indicators

That is probably a ton of work to implement, so it is just really an informative question about whether this has been considered already and whether that is of interest. I believe that having a compiler take care of all those subtle optimizations would be a terrific validation of Svelte approach.

Also putting this here for reference: #3203.

Describe alternatives you've considered
I am unaware of the existing patterns by which this is achieved with Svelte currently. I believe Sapper is taking care of smooth transitions between pages in a SPA, which is one of the use cases React folk cover with a clever use of Relay. They basically fetch the data and the page component in parallel, before doing any rendering. That is possible because they know ahead of time (gathered from the declaration colocated in component) what to fetch.

I am not current with Sapper though so can't tell the extent to which Sapper helps devs ensure a smooth user experience neither compare both approaches.

How important is this feature to you?
Can't say it is important in the sense that I have nothing depending on it. But this is nice to have for sure.

Additional context
That is a talk where the Facebook/React/GraphQL approach is described: https://www.youtube.com/watch?v=Tl0S7QkxFE4

@Conduitry
Copy link
Member

There's not currently any groundwork at all for doing cross-component optimizations - each component is compiled in isolation. I don't think there's anything particularly currently actionable for Svelte to do here. Sapper is solving this using preload. It's also probably possible to implement something like this with a centralized userland 'data fetcher' module, with which each component registers the data it needs, and which exposes a store or stores that every other component can subscribe to. From what I'm understanding of this issue, this seems far too opinionated to have a solution baked into Svelte itself - and is much more suited for an application framework built on top of Svelte.

@brucou
Copy link
Author

brucou commented Apr 29, 2020

I agree with much of what @Conduitry says. Developers can implement manually any of the optimizations that I am describing. My belief is that, because there are a lot of repetitive tasks involved, and some patterns are better than others, a compiler may provide better developer experience in the implementation of those data optimizations.

I also agree that a lot of thinking is required before baking stuff in Svelte core. Once you add it, you can't remove it, and you have to maintain it. If you change ideas later, you broke tons of people code. The problem is that it is not possible to provide a nice developer experience, like having a dedicated special tag, without hooking into the compiler, which Svelte does not allow so far. Vue will in Vue 3 allow developer to hack the template compiler, with the possibility to extend syntax. That is very much in beta and pretty much nobody is using it, so this is really bleeding edge stuff. Might be worth keeping in some parts of the brain for the future.

Anyways let's see what others say.

@Conduitry Conduitry added the awaiting submitter needs a reproduction, or clarification label Apr 30, 2020
@jonatansberg
Copy link

jonatansberg commented May 11, 2020

I think there might be an opportunity to do something like Next's getServerProps and getStaticProps by extending Sapper and/or the Svelte component syntax slightly. Imagine that we would have one (or two) new script scopes that can be added to our Svelte components specifically for build and server only execution.

In many ways I think this is a much more elegant solution than "magic static methods" and dead code elimination since it fels like the risk for accidentally leaking server secrets would be way lower.

<script context="build">
  // any code in here would be pre-evaluated at build time,
  // and then added to the module scope(?)
</script>
<script context="server">
  // any code in here would run during SSR, and for client builds 
  // it could be exposed as middleware that could be consumed 
  // by a server or exposed as serverless function(s)

  export default async (req, res, context) =>  { ... }
</script>

The coordination part for the server-context would also need to be solved. Thats probably the harder problem.. It's possible that limiting the use to "routes" and/or pages, like preload in Sapper, could be a good enough solution, as long as there is some way of composing the server dependencies (i.e. an equivalent to calling preload on a child component).

@brucou
Copy link
Author

brucou commented May 26, 2020

I was just reading about Svelte in Markdown and I realized that there is a preprocessor option in Svelte. That could probably be used to pre-compile away the extra tag that I was proposing. Going to investigate that.

@igorovic
Copy link

igorovic commented Sep 1, 2020

I checked if it's possible to extend <script context="..."> with some custom context. Seems the good way to go for me.
But there are some blocking points.

  1. Svelte parser allow only context="module". src/compiler/parse/read/script.ts#L21
  2. Eventualy svelte compiler renders and generate a component. src/compiler/compile/index.ts#L93

I am still wondering what would be a clean way to extract the additional context and pass it to the bundler (rollup in my case).
Does any core team member has an advice?

Here is my experimental fork.
igorovic@53a9c71

@jonatansberg
Copy link

@igorovic Awesome!

A colleague of min mentioned that @lukeed posted an RFC for something like this yesterday. Maybe we should try to move this conversation over there?

sveltejs/rfcs#27

@PaulMaly
Copy link
Contributor

PaulMaly commented Oct 19, 2020

@brucou Сorrect me if I'm wrong, but it just looks like synchronous component rendering right after data fetching. Sort of, we want to display the component only when the data it needs is loaded. Since the component can be dynamic (code-splitted), so data load function must be part of that component. If so, then the preload in Sapper quite handles such things. You also can do the same on a pure Svelte, using loadable components like svelte-viewpoint:

// UserProfile.svelte

<h1>Hello {user.name}</h1>

<script context="module">
  export async function preload({ userId }) {
    const res = await fetch(`/users/${userId}`);
    const user = res.json();
    return { user };
  }
</script>
<script>
  export let user = null;
</script>

// somewhere in the app

<script>
  import Viewpoint from 'svelte-viewpoint';
  ...
</script>

<Viewpoint component={() => import('./routes/UserProfile.svelte')} {userId}>
  <div slot="loading">
    <Spinner />
  </div>
  <div slot="error" let:error>
    <Error {error} />
  </div>
</Viewpoint>

svelte-viewpoint router-agnostic and can be used with any routing solution. Implementation of such things in user-land is too simple to be baked into Svelte.

@brucou
Copy link
Author

brucou commented Oct 19, 2020

Hi @PaulMaly thanks for your answer. I did not know about svelte-viewpoint and it is great that you mentioned it.

My original post was about the possibility to use a compiler to aggregate data fetches that are scattered in components for optimization purposes. Optimization-aside, we can indeed fetch data as you recommend. Optimization may include not refetching data that has already ben fetched, aggregate fetches into a single request, and more. The inspiration here is graphQL/Relay.

I thought a compiler would add value here because data fetching and the related issues are a common and generic need that can be abstracted behind an API (output by the compiler). Most of the state handled by apps (app local state that can be accessed by the app synchronously) I have seen are 80% composed of copy of remote state (state in databases that can be only be accessed asynchronously). The source of truth being the remote state, such apps more often than not need a synchronization layer. Synchronization of data sources is one of the generic problems I alluded to. In the React world, React Query is gaining in popularity as it handles fetching, caching and synchronization. That is one thing.

The other thing is optimization and the inspiration here is graphQL and its client libraries (Relay for instance). Apollo handles optimistic fetching, caching, refetching, error handling, pagination, and more. The same way you do code-splitting and code bundling, you can do data-splitting, and query bundling. So the data requirements for the required components for a page may be aggregated in a lower number of more efficient queries (that is what I called query bundling). If the user is querying data in a component that is never used, then the unused data can be trimmed out of the query (dead data elimination :-). The idea is that if the compiler knows at build time (via some declarative API or syntax under the <remote/> or <query/> or <fetch/> or whatever-the-name tag) the shape of the data that is required, it can apply some optimization and write some boilerplate so the developer does not have to. I haven't been much farther in the thinking about syntax and APIs though. I guess my proposal is a query language (that goes under a custom tag to define) and that is statically analyzable.

That would fit Svelte idea of developer productivity and writing less code and performant-enough by default. Implementation would not have to be baked in Svelte, but the API (or declarative language) would have to be imposed by Svelte. Just like there are several implementations of graphQL but one specification for it. An implementation could be passed through CLI options.

So this differs from @jonatansberg proposal of varying <script context> tags (this being a script, it is hard to statically analyze), though I think jonatan's proposal is awesome. Maybe you could have both in one go:

<remote lang=js context=server> 
  // any code in here would run during SSR 
<remote/>

and

<remote lang=graphql context=build endpoint=...> 
  // some graphql query
<remote/>

? I need to flesh my ideas out a bit (or a lot) more I guess.

In any case, I don't really support Svelte baking this in, but rather Svelte coming up (if it does not already have) an architecture that respects the open/closed principle, i.e. open for extension, closed for modification. The subsuming idea in fine is the ability of adding custom tags and integrating custom processing within the svelte toolchain --- just like you have hooks, lifecycle methods, middleware or plugins that add custom processing in pre-determined integration points. Svelte preprocessor is a good example of allowing custom processing. Svelte Native, Svelte GL, Svelte TypeScript may be other examples of possible extensions that would benefit from reusing Svelte Core without modification (if they don't already do).

@brucou
Copy link
Author

brucou commented Oct 21, 2020

One example (using graphql) of what a declarative description of a query could be in Svelte with a custom tag, and its possible preprocessing:

  1. Custom tag (client side)
<remote preload lang='graphQL' endpoint=...  >
  query events($page: Int){
    events(page: $page) {
      name
    }
  }
</remote>
<script>
async function fetchMoreEvents(){
  page++;
  await events.fetchMore({
    variables: {page},
    updateQuery: (previousResult, {fetchMoreResult}) => {
      return {events: ...previousResult.events, ...fetchMoreResult.events}
    }
  })
}
<script/>

{#await events}
  Loading...
{:then result}
  {#each result.data.events as event}
    {event.name}
  {/each}
  <SvelteInfiniteScroll on:loadMore={fetchMoreEvents}>
{:catch}
...
{/await}
  1. After preprocessing:
<script>
import {query} from 'svelte-apollo'
import {client} from './client'
import {gql} from 'apollo-boost'
import SvelteInfiniteScroll from 'svelte-infinite-scroll'

const _query = gql`
  query events($page: Int){
    events(page: $page) {
      name
    }
  }
`
let page = 0;
const events = query(client, {query:_query, variables: {page}})

async function fetchMoreEvents(){
  page++;
  await events.fetchMore({
    variables: {page},
    updateQuery: (previousResult, {fetchMoreResult}) => {
      return {events: ...previousResult.events, ...fetchMoreResult.events}
    }
  })
}
</script>

{#await events}
  Loading...
{:then result}
  {#each result.data.events as event}
    {event.name}
  {/each}
  <SvelteInfiniteScroll on:loadMore={fetchMoreEvents}>
{:catch}
...
{/await}
<script/>

I have a few things to flesh out (the ./client that should create a graphQL client from the endpoint parameter). The implementation of graphQL (here apollo/gql) can be passed via CLI? Stuff to think about. But the first stage is to make querying more declarative and generate the code from the query declaration. Second stage is optimization.

The preload parameter is to indicate that this should be taken into account server side. I am not sure, since it is not done that way in sapper, that the {#await promise} can be rewritten so as to keep only the part in the {:then} clause. If that would be possible, then there would be no extra code to write for the server.

@stale
Copy link

stale bot commented Jun 26, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale-bot label Jun 26, 2021
@stale stale bot removed the stale-bot label Jun 26, 2021
@brucou brucou closed this as completed Jun 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting submitter needs a reproduction, or clarification temp-stale
Projects
None yet
Development

No branches or pull requests

6 participants