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

RFC: svelte-urql: enable to query in SvelteKit load() function #1819

Closed
MichaelBailly opened this issue Jul 21, 2021 · 40 comments
Closed

RFC: svelte-urql: enable to query in SvelteKit load() function #1819

MichaelBailly opened this issue Jul 21, 2021 · 40 comments
Labels
future 🔮 An enhancement or feature proposal that will be addressed after the next release

Comments

@MichaelBailly
Copy link

Summary

SvelteKit allows ServerSide rendering of the web application pages. One of the main benefit is to be able to load backend data from the server, have it rendered as HTML. The SvelteKit way of doing it is by adding the pre-rendering code in the <script context="module"> part of the page.

One ideal setup would be to:

  • in script context="module": instanciate the urql client
  • in script context="module": create the operationStore
  • in script context="module": query(operarionStore)
  • in script context="module": pass the operationStore to component through props
  • in script: get back the component from store

However, Svelte Contexts are not available there. Which means the query(operationStore) cannot run, because it fetches the urql client through svelte contexts here.

Having a way to pass the urql client to the query function would allow such use case.

Proposed Solution

Change the query function signature from:

query<Data = any, Variables = object>(
  store: OperationStore<Data, Variables>
): OperationStore<Data, Variables>

to

query<Data = any, Variables = object>(
  store: OperationStore<Data, Variables>,
  opts?: {
    client?: Client
  }
): OperationStore<Data, Variables>

This would allow to either use query like now:

const todos = operationStore(todoRequest);
query(todos);

or to use specify a custom client

const client = createClient({ url: 'http://localhost:3000/graphql' });
const todos = operationStore(todoRequest);
query(todos, { client });

Having that in place, SvelteKit users can easily plug their code:

<script context="module">
const client = createClient({ url: 'http://localhost:3000/graphql' });
const todos = operationStore(todoRequest);
query(todos, { client });

return {
  props: {
    client, // pass the client to the component
    todos
  },
  context: {
    client // pass the client to sub-pages 'context="module"' part, example if we are in a __layout.svelte 
  }
};
</script>

<script>
import { setClient } from '@urql/svelte';
export let client;
export let todos;

// let's say it's the root __layout.svelte, setting the client here
// allows to use query(store) directly in all components thereafter
setClient(client); 
</script>

{$todos.data.todos.length}

Please tell me what you think about this proposal.

@MichaelBailly MichaelBailly added the future 🔮 An enhancement or feature proposal that will be addressed after the next release label Jul 21, 2021
@disbelief
Copy link

Question: is @urql/svelte usable with SvelteKit at all currently?

I'm seeing Error: Function called outside component initialization when calling getClient from a svelte component. This is in the regular <script> tag and not <script context="module">:

<script>
  // src/routes/index.svelte

  import { onMount } from 'svelte';
  import { getClient } from '@urql/svelte';

  let client;

  onMount(() => {
    client = getClient();
  });
</script>

I saw in the docs this error might happen thanks to vite, but have added the recommended config change:

// svelte.config.js

const config = {
  kit: {
    vite: {
      optimizeDeps: {
        exclude: ['@urql/svelte']
      }
    }
  }
};

export default config;

@frederikhors
Copy link
Contributor

I'm using "@sveltejs/kit": "1.0.0-next.155" without optimizeDeps at all and it works amazingly!

@disbelief
Copy link

I'm using @sveltejs/kit: ^1.0.0-next.158 => 1.0.0-next.158 and it's not working with or without the optimizeDeps

@disbelief
Copy link

Seems it was due to calling initClient and getClient inside onMount. Moving them to the top scope of the svelte component removed the error message.

@disbelief
Copy link

I'm wondering what the current state of @urql/svelte + SvelteKit + SSR is?

It would be fantastic to get a canonical example of how to do it, particularly around the SvelteKit load function and the urql client.

I'm using a strategy based off of this gist: https://gist.github.com/sastan/85cf3d011b152a80d3a5933a09df21f6
(without the response normalization stuff).

The __layout creates an instance of the urql Client and stores it in stuff (SvelteKit renamed the loader context to stuff in latest version).

Then the route's load function uses client.query to create the OperationStore and await the results of the query. Then it provides the operationStore as a prop to the Svelte component. The Svelte component then calls query(operationStore) on the prop it receives.

This seems to work most of the time, but variable reactivity doesn't work. Specifically, changing the variables doesn't seem to trigger a new request.

I've tried both $store.variables = nextVariables and store.set({ variables: nextVariables }) with and without refetch(). The operationStore does get written to but the actual client request's variables don't change and no new request is sent.

Guessing it has something to do with the way the OperationStore is created in the loader, passed to the component, and query() called upon it, but I've walked through the debugger and can't figure out what the problem is.

Also the above solution seems a bit kludgy and I'm guessing (hoping) there's a more straightforward way to do this?

@JoviDeCroock
Copy link
Collaborator

JoviDeCroock commented Oct 16, 2021

I've been giving this some thought and well, it's a tough one. The issue being that the context doesn't work during ssr which means that methods behave differently in ssr vs csr. Secondly there isn't really a way to take the ssr-data and put it in an hydratable location for svelte-kit.

An "ideal" workflow would be the following:

  • we create a client with the ssrExchange in __layout
  • we query the data we need in load
  • we reuse this client during the normal renders through context/something else?
  • after everything completes we use ssrExchange.extractData() which enables us to rehydrate all this data on the client

This would set us up for a seamless integration, as far as I can tell, no actual data-fetching library seems to have found a way to integrate with svelte-kit 😅

Changing the signature alone would not be off much use as this would tie too much into reactivity meaning there is no good heuristic to tell if all data has been loaded, hence why in Node Svelte opts for the async load helper rather than the normal render method.

@disbelief
Copy link

The ssrExchange approach is interesting, I wasn't aware of that exchange.

It sounds similar to the gist I posted: the urql client executes the query in SSR, serializes the data, and the urql client is passed to the CSR a prop.

I'm doing this now and it's not 100% working with reactivity, but it does seem to use the serialized responses from the client prop.

It's probably worthwhile for me to post my exact code:

// __layout.svelte

<script context="module">
  export const load = ({ fetch, stuff }) => {
    const client = createClient({
      fetch,
      uri: '/graphql', 
      // ... other config
    });

    async function query(
      queryDocument,
      variables,
      context = {}
    ) {
      let store = operationStore(query, variables, context);
      const result = await client.query(store.query, store.variables, store.context).toPromise();
      return Object.assign(get(store), result);
    }

    return {
      stuff: {
        ...stuff,
        client,
        query
      },
      props: { client }
    };
  };
</script>

<script>
  import { setClient } from '@urql/svelte';

  export let client: Client;

  setClient(client);
</script>

<slot />

Possible problems with the above:

  • serialization/deserialization of the client from SSR to CSR breaks something
  • using Object.assign on the operation store in the query func to add the result
  • using urql/svelte's query(operationStore) on an operation store that was initialized in this way

@disbelief
Copy link

actual data-fetching library seems to have found a way to integrate with svelte-kit 😅

Yikes that's not great news. Someone from SvelteKit should probably be looped in on this.

@JoviDeCroock
Copy link
Collaborator

JoviDeCroock commented Oct 17, 2021

I think I found a solution @disbelief it's a bit hacky but shows the general idea 😅 JoviDeCroock/urql-svelte-kit@fe55295 someone with more svelte-knowledge might be able to clean this up 😅

@frederikhors
Copy link
Contributor

Can we please contact someone from SvelteKit's team?

@disbelief
Copy link

@JoviDeCroock thanks this is a promising approach! It would take me some time to refactor my app like this, so I can't confirm it works for me just yet.

@disbelief
Copy link

@Rich-Harris wondering if you could provide any insight or know a good person on the SvelteKit team we could ask about this?

@nimmolo
Copy link

nimmolo commented Oct 29, 2021

I feel like @bluwy may know a lot about GraphQL and SvelteKit... the contributors over there seem super busy though

@bluwy
Copy link

bluwy commented Oct 31, 2021

Hey 👋 I've not used urql before but I can give some thoughts on this.

Re: SvelteKit/Vite optimizeDeps workaround

Those should be fixed since a couple months ago, we've fixed Vite's pre-bundling in vite-plugin-svelte side which resolved most of the quirks in the past.

Re: graphql query and serialization flow

I've actually written a blog about Apollo and Sapper before, but needs to be adapted to urql and SvelteKit.

Looking back at the blog, I actually prefer Method 1 now and it seems to be closer to what most of the discussion here proposes. I'm not sure if the trick shown works in SvelteKit anymore, but if it does I'd be interested to know.

Re: RFC

I think it makes sense to allow querying in context="module" too since the data could be one-off and it makes sense to do that way. Otherwise if you would expect to refetch the data whenever some state changes, querying in the component instance may be easier to handle that. Which with a mix of Method 1 above, should be possible. Take my opinion with a grain of salt though.

@AndreasHald
Copy link

Just throwing in my two cents here, since I have been facing this same issue with the Apollo client and are looking towards creating some samples using Urql instead to propose the switch in our app.

Svelte kit actually caches fetch calls in the load function to make sure it does not make the call again on the client. What I did using Apollo client was simply instantiating the client using the fetch method provided in the load function, and then using the client to query the api both during ssr and csr svelte kit then automatically recognizes that the second fetch request on the client is identical to the one performed during ssr and serves the "cached" response.

@disbelief
Copy link

This makes sense: no need to do our own request/response serialization when SvelteKit's fetch does it out of the box already.

But for some reason things get messed up (for me) when working with urql OperationStores that are initiated in the load function and interacted with in the client (eg. changing variables, refetching, etc).

Perhaps instead of creating the OperationStore and returning it as a prop, the load function should just execute the query during SSR so it's serialized into the client's fetch function?

// src/routes/__layout.svelte

<script context="module">
  import { createClient } from '@urql/svelte';

  export async function load({ fetch, stuff }) {
    const client = createClient({
      // Pass in the fetch from sveltekit to have access to serialized requests
      fetch,
    });

    return {
      stuff: { ...stuff, client },
      props: { client }
    }
  }
</script>


// src/routes/myRoute.svelte

<script context="module">
  import { browser } from '$app/env'

  export async function load({ stuff }) {
    if (!browser) {
      stuff.client.query(SOME_QUERY, SOME_VARIABLES);
    }
    return {
      props: {}
    };
  }
</script>

<script>
  import { query, operationStore } from '@urql/svelte';

  const opStore = operationStore(SOME_QUERY, SOME_VARIABLES);
  // should use the serialized result in the client if the query+variables are the same
  query(opStore);

</script>

@AndreasHald
Copy link

AndreasHald commented Nov 18, 2021

Alright I believe I got the same idea working.

In the layout file, you create the client, and return it as a prop, you can then set it on the context, so all child components can retrieve it, you also store it in "stuff" so your subsequent load functions has access to it, you create it using the svelte-kit provided fetch, so that it may use svelte-kit caching

<!--__layout.svelte-->
<script lang="ts" context="module">
	import type { Load } from '@sveltejs/kit';
	import { createClient, setClient } from '@urql/svelte';
	export const load: Load = async ({ fetch }) => {
		const client = createClient({
			url: '/graphql',
			fetch
		});

		return {
			stuff: {
				client
			},
			props: {
				client
			}
		};
	};
</script>

<script lang="ts">
	export let client;
	setClient(client);
</script>

<slot />

On on routes you can now

<!-- /someroute/index.svelte -->
<script lang="ts" context="module">
	export const load: Load = async ({ stuff }) => {
		const store = operationStore(`
		    query {
		      tasks {
		        name
		      }
		    }
		  `);

		await stuff.client.query(store.query, store.variables, store.context).toPromise();

		return {
			props: {
				store,
			}
		};
	};
</script>

<script lang="ts">
	import type { Load } from '@sveltejs/kit';
	import { operationStore, query } from '@urql/svelte';

	export let store;
	query(store);
</script>

Your store will then have the value server side rendered but not make multiple requests :)

Edit: Ah just missed your solution also @disbelief

@disbelief
Copy link

@AndreasHald this is what I was doing but it runs into problems when modifying variables or refetching on the client side.

Just realized after posting my above suggestion that it won't work because the urql query function doesn't use the custom client that we instantiate in the layout.

@disbelief
Copy link

Perhaps the ideal solution is to be able to provide a client (or fetch) to urql's query or operationStore functions?

@AndreasHald
Copy link

@disbelief Yeah it seems you need to return it as a prop and call setClient in the __layout file however, it seems to work just as expected for me when updating variables.

@disbelief
Copy link

Interesting. Perhaps that's a red herring / bug coming from elsewhere in my code.

@AndreasHald
Copy link

@disbelief how are you reassigning variables?

This works for me
Screenshot 2021-11-18 at 16 20 34

However this does not work
Screenshot 2021-11-18 at 16 20 47

But I think that's a svelte thing, where the "reactivity" is not aware that variables has changed unless you actually reassign it, in the example that does not work the variable being reassigned is actually variables.index however I could be wrong.

@disbelief
Copy link

Yep, aware that the entire variables reference needs to be replaced.

I tried these two ways:

$opStore.variables = { ...variables, page: page + 1 };

and

$opStore.set({ variables: { ...variables, page: page + 1 } });

@disbelief
Copy link

disbelief commented Nov 18, 2021

My current solution is to issue a brand new operation store+query on the client to fetch the subsequent pages, vs reusing the operation store that the load function provides. Which obvs kinda sucks.

@jdgamble555
Copy link

jdgamble555 commented Nov 21, 2021

I figured it out. Please post this official docs as it follows the same pattern as the other frameworks.

  • 1st, don't use @urql/svelte, use @url/core for sveltekit

urql.ts

export const ssr = ssrExchange({
    isClient: !isServerSide,
    initialState: !isServerSide ? window['__URQL_DATA__'] : undefined,
});
...
const client = createClient({
    url: `YOUR URL`,
    exchanges: [
        dedupExchange,
        cacheExchange,
        ssr,
        ... YOUR EXCHANGES
        fetchExchange
    ]
});

export const data = `
<script lang="ts"> 
  window.__URQL_DATA__ = JSON.parse(JSON.stringify(__SSR__)); 
</script>
`;

export default client

index.svelte

<script context="module" lang="ts">
  import { dev, browser } from '$app/env';
  import { ssr, data, client } from '../modules/urql';

  export async function load() {
    if (browser) {
      ssr.restoreData(window['__URQL_DATA__']);
    }

    let r = await client.query(....).toPromise();

    const d = browser
      ? 'null'
      : data.replace('__SSR__', JSON.stringify(ssr.extractData()));

    return { props: { r, d } };
  }
</script>
<script lang="ts">

export let d: any;
export let r: any[];
...
</script>

{@html d}

Hope this helps someone,

J

@gmanfredi
Copy link

gmanfredi commented Dec 5, 2021

Hey @jdgamble555,
Thanks, that was very helpful! I've had this working in Sapper / Apollo, but with the help of this discussion, now in SvelteKit and URQL (much preferred over Apollo). For me, it was particularly important for fluid page animations since the target element needs to be already rendered on target page to have the transition work.

Regarding your example index.svelte, I think you need to add a toPromise() on the query, and since you exported client as default in urql.ts, you need to import client outside of the destructuring assignment. I have my module script working like this:

<script context="module">
  import { browser } from "$app/env"
  import client, { ssr, data } from "@/graphql/urql"
  import GET_POST_DETAIL from "@/graphql/queries/getPostDetail"

  export const load = async ({ page: { params } }) => {
    const { id } = params

    if (browser) ssr.restoreData(window["__URQL_DATA__"])
    let result = await client.query(GET_POST_DETAIL, { id }).toPromise()
    const _SSRData = browser ? "null" : data.replace("__SSR__", JSON.stringify(ssr.extractData()))

    return {
      props: {
        _SSRData,
        post: result.data?.getPost,
        id
      }
    }
  }
</script>

My next task is to get subscriptions working on the rehydrated client.

@disbelief
Copy link

While the above solution is appreciated and seems promising (thank you @jdgamble555), I'd argue that the goal of the @urql/svelte package should be to support SvelteKit out of the box, as it will be the most common use-case of Svelte and GraphQL going forward. So, we still need a canonical "SvelteKit way" of doing this.

@jdgamble555
Copy link

@gmanfredi - Yes, you need toPromise(). I was simplifying my code and missed that! (You also should be using typescript, but that is your preference ;) )

@disbelief - @urql/svelte simply cannot work with sveltekit the way it currently stands, as it was not written to work with SSR (although it should have so it could work with Sapper). That being said, you can use @urql/core without problems. You can actually do this with Angular as well, although they don't claim to support it (I have written apps with urql/Angular).

Since the fetch command in sveltekit automatically saves the data (I think using cookies?), you should be able to replace the fetch in URQL somehow with the Sveltekit fetch version, as the sveltekit way of doing things.

I have not tested it, but it should be possible like I did here. Let me know if someone simplifies this!

J

@disbelief
Copy link

@jdgamble555 yes the solutions myself and others posted above do what you describe -- passing the SvelteKit-provided fetch function to the urql client. It seems to mostly work, but I was having some issues when creating the OperationStore in the load function, and then passing it as a prop to the component. Not all of the OperationStore's functionality worked as expected.

@jdgamble555
Copy link

Hi @disbelief. I was talking about using the svelte fetch that does all this automatically and passing it to the exchanges as fetch, which you can change. The examples above are using @urql/svelte and operationStore. I don't see how @urql/svelte can work at all with the ssr restrictions.

I think if you pass in the fetch to @urql/core, like I did in my angular example , it might work.

J

@Rich-Harris
Copy link

Hi all — I was tagged earlier so have been dimly aware of this thread happening; sorry for not weighing in sooner. I will read the whole thing when I get a chance!

In the meantime, I just wanted to make you aware of this: sveltejs/kit#2979. It's a proposal for (hopefully) making it easy for things like svelte-urql hook into load for SSR purposes. I think it would work similarly to the Angular example above? Would love to know if you have thoughts on whether this API would meet your needs (feel free to comment on that issue), and apologies if it's a complete red herring.

@disbelief
Copy link

@jdgamble555 yes it's possible to provide Svelte's fetch to @urql/svelte:

  1. Pass Svelte's fetch to the createClient function in load
  2. Return that client as a prop from load
  3. Call setClient with that prop in the component

The above should use the sveltekit-fetch-enabled client for all @urql/svelte functions including the creation of OperationStores, etc.

@disbelief
Copy link

disbelief commented Dec 9, 2021

@Rich-Harris thanks for weighing in, I'm sure you're pretty busy so appreciate the reply. The linked proposal looks like a quite useful convenience helper.

Generally what I was looking for is a well-defined convention for using an external client in SvelteKit's load function that works as expected under both SSR + browser, and takes advantage of the serialized requests in SvelteKit's fetch.

Perhaps the current "monstrosities" approach you outlined in that issue is the canonical way of doing this at the moment?

@Rich-Harris
Copy link

It is the canonical approach right now, yes (or at least it's the approach I've been taking). Given the positive feedback on the proposal so far, I'm keen to make a start on it fairly soon, so hopefully it won't be canonical for long

@rogueyoshi
Copy link

@jdgamble555 yes it's possible to provide Svelte's fetch to @urql/svelte:

  1. Pass Svelte's fetch to the createClient function in load
  2. Return that client as a prop from load
  3. Call setClient with that prop in the component

The above should use the sveltekit-fetch-enabled client for all @urql/svelte functions including the creation of OperationStores, etc.

Will this solution allow proper prerendered routes using the query information?

@disbelief
Copy link

@rogueyoshi in my experience, yes it works.

@jdgamble555
Copy link

@disbelief - I was saying I cannot see how @urql/svelte would work in sveltekit, not whether or not passing the fetch would work. I was simply stating that what was mentioned above, was not the same thing.

Either way, the problem is that the fetch function only exists in the load function. That suggests you need to repass the fetch to the load function for every single component, instead of doing it one time through out your app. That is a problem.

Sharing a ts or js file that already loads the client function one time, would be the best practice. I am wondering if it might be better to just create the same cookie that the fetch function does internally (not sure what it does exactly under the hood) so that you are not limited to the load function.

Anyone have any thoughts not being limited to the load function?

J

@disbelief
Copy link

@jdgamble555

you need to repass the fetch to the load function for every single component, instead of doing it one time through out your app.

You can do what I said above just once in the __layout so that the correct client with overridden fetch is used in any component rendered by a route. The load functions of specific routes can also receive this client in stuff, if they need it.

This gist might provide a bit more clarity though it uses the old "context" naming instead of stuff and does a few additional unnecessary things around normalizing the GraphQL responses: https://gist.github.com/sastan/85cf3d011b152a80d3a5933a09df21f6

@chbert
Copy link

chbert commented Feb 4, 2022

Is there any update on this issue?

@kitten
Copy link
Member

kitten commented May 24, 2022

Obsolete and potentially resolved by: #2370
The API basically now just exposes store factories that are a transparent conversion from sources to a readable store.

@kitten kitten closed this as completed May 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
future 🔮 An enhancement or feature proposal that will be addressed after the next release
Projects
None yet
Development

No branches or pull requests