-
Notifications
You must be signed in to change notification settings - Fork 462
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
feat(svelte): allow to pass a client directly to query
, ...
#1527
Conversation
🦋 Changeset detectedLatest commit: 582321b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
c5394a4
to
1967b4d
Compare
…on` and `mutation`. This change supports to use the svelte bindings outside of a component.
1967b4d
to
582321b
Compare
This change has been proposed before, but we don't want to support this explicitly because there are two other ways to cover this. The first method is to initialise the storage and query and use The second, which is what you're trying to address are imperative calls. Imperative one-off calls are discouraged for queries because they'll be unable to update their data, i.e. listen to changes, but can already be done using So this inclusion hasn't been made because it'd encourage users to use |
I do not want to pause the query. I want it to be executed right away and I need a promise to know when the data has been loaded. That way when the component renders initially it already has the data which allows SSR with sveltekit to work.
I only want the data to be available during initial rendering. After that normal bindings are used to trigger any updates.
What would be the recommended way for async load/prefetch for sveltekit? Edit: My current hack looks like this: <script context="module">
import { get } from 'svelte/store'
import { operationStore, query } from '@urql/svelte'
import * as gql from './queries'
export async function load({ page, context }) {
const store = operationStore(gql.QueryDocument, queryToVariables(page.query))
const result = await client
.query(store.query, store.variables, store.context)
.toPromise()
return { props: { result: Object.assign(get(store), result) } }
</script>
<script>
export let result
query(result)
// normal use – with initial data already available
</script> |
I wonder if this introduces a pitfall, I'm not very familiar with how SvelteKit works so forgive me if this assumption is wrong. That Or is Your workaround looks similar to how stuff is done in |
partial results: Not relevant for me at the moment. But would it be possible to wait until the data is not mutation: That is why I want to have an observable stream. During SSR I do not care. But on the client the result should be updated. That is why I try to use the Suspense: That would be great but not available yet. The current best practice is using the Below is an outline of how I would like to use urql with <script context="module">
import { operationStore, query } from '@urql/svelte'
import * as gql from './queries'
export async function load({ page, context }) {
const result = query(operationStore(gql.QueryDocument, queryToVariables(page.query)), context.client)
// TODO await until the store has fetched the data; maybe wait until the full response is fetched (not stale)
// THIS IS JUST A PLACEHOLDER
await result.ready()
return { props: { result } }
</script>
<script>
export let result
// normal use – with initial data already available
</script> |
Maybe the function prefetch(store) {
let unsubscribe
return new Promise((resolve, reject) => {
unsubscribe = store.subscribe((result) => {
if (result.fetching) return
if (result.stale) return
if (result.error) {
reject(result.error)
} else {
resolve(store)
}
})
}).finally(() => unsubscribe?.())
} This function could be part of @svelte/urql or in user land. <script context="module">
import { operationStore, query } from '@urql/svelte'
import * as gql from './queries'
export async function load({ page, context }) {
const result = query(operationStore(gql.QueryDocument, queryToVariables(page.query)), context.client)
// await until the store has fetched the data
await prefetch(result)
return { props: { result } }
</script>
<script>
export let result
// normal use – with initial data already available
</script> |
Hm, I so enjoy the idea of a native What I'm struggling with is the lack of "app-wide" abstractions for SSR. Admittedly, this is shaky in React as well and only Vue 3 has a solid concept of making such operations baked into the "normal" lifecycle of apps so far. Just, so we get this right, let me try to reiterate this. The I think |
I hear you.
Yes! It is used on the server and the client.
The ssrExchange is not needed for sveltekit. Sveltekit provides a fetch method (same API as global fetch, available on server and client) to load. I use that to create the urql client. On the server sveltekit tracks the requests made through fetch. On first render (re-hydration) on the client sveltekit re-uses the previous/server fetch result. |
After looking more carefully at the function query(store) {
// This subscription keeps the store updated
const subscription = pipe(
toSource(store),
// ...
);
// Cleanup on unmount
onDestroy(subscription.unsubscribe);
return store;
} My proposed solution would not work as there is not a current component (required by Maybe my hack (#1527 (comment)) is the way to go for now? Have you ever considered making the subscription lazy/on-demand? Start/Stop the subscription based on subscribers to the store? Something like this keeps could work: import { readable } from 'svelte/store'
export function query(store, client = getClient()) {
const { subscribe } = readable(store, (set) => {
// Called when the number of subscribers goes from zero to one (but not from one to two, etc)
const subscription = pipe(
toSource(store),
// ...
subscribe(update => {
_markStoreUpdate(update);
store.set(update);
// Just for now – there are surely better ways
set(store);
})
);
// Cleanup when there are no more subscribers
return subscription.unsubscribe
})
// Just for now – there are surely better ways
return Object.create(store, {
subscribe: { value: subscribe }
});
} |
That's actually exactly how it works; that's just in the Core Like I said, it's not hard to make this work. We'd start a source on the client and attach it to a promise (just like The PR I mentioned can also help here since it'd then not even matter whether the prefetch is started before, after, or while the component is mounted. |
I think I have a better understanding of the bindings package now. Please correct me if I'm wrong here:
As I understand my proposed change would therefore not work as the use needs to call Side note: I would prefer not to have the non-reactive bindings because the reactive prefix Maybe it would be possible to add an auto-subscription method . This wouldn't support the non-reactive prefix access – one must use import { readable } from 'svelte/store'
// The name is just a placeholder
export function autoquery(store, client = getClient()): Writable<OperationStore> {
const { subscribe }= readable(store, (set) => {
// Called when the number of subscribers goes from zero to one (but not from one to two, etc)
// Shared code with query could be re-factore into an helper method
const subscription = sharedCodeFromQuery(client)
// Propagate changes
const unsubscribe = store.subscribe(set)
// Cleanup when there are no more subscribers
return () => {
subscription.unsubscribe()
unsubscribe()
}
})
// Maybe expose some additional properties
return {
set: store.set,
update: store.update,
subscribe,
}
}
export function prefetch(store: OperationStore): Promise<OperationStore> {
let unsubscribe
return new Promise((resolve, reject) => {
unsubscribe = store.subscribe((result) => {
if (result.fetching) return
if (result.stale) return
if (result.error) {
reject(result.error)
} else {
resolve(store)
}
})
}).finally(() => unsubscribe?.())
} This would allow my use case like this: <script context="module">
import { operationStore, autoquery, prefetch } from '@urql/svelte'
import * as gql from './queries'
export async function load({ page, context }) {
const todos = autoquery(operationStore(gql.TodosDocument, queryToVariables(page.query)), context.client)
// await until the store has fetched the data
await prefetch(result)
return { props: { todos } }
</script>
<script>
export let todos
// initial data already available
// only reactive binding is supported
</script>
{#if $todos.fetching}
<p>Loading...</p>
{:else if $todos.error}
<p>Oh no... {$todos.error.message}</p>
{:else}
<ul>
{#each $todos.data.todos as todo}
<li>{todo.title}</li>
{/each}
</ul>
{/if} |
Great!
|
Re: the reactive binding bit; that's actually necessary. We want to preserve state (hence the store is created once) and we want to observe changes to it, so the query is like a "directive" that watches the store for its inputs and updates the outputs. Hence it doesn't need to be reactive (although in some circumstances it could be part of a reactive statement) I'll try to find some time to write an RFC to outline the proposed That said, that means I'll close this PR for now. But until then we can keep using this thread to keep up the discussion until the RFC is ready ✌️ I'll try to get to the RFC this week, hopefully early this week. Even if you decide not to pick up the implementation, I'll post it here so you can take a look! Cheers 🙌 |
Sounds good! Just for clearifacation: The
The problem I'm currently trying to solve is that 1 and 4 are not possible within sveltekit What I was proposing is another method that does not support The import { readable } from 'svelte/store'
// The name is just a placeholder
// `client = getClient()`: to support use within a component initializer -> `autoquery(store)`
export function autoquery(store, client = getClient()): Writable<OperationStore> {
const { subscribe }= readable(store, (set) => {
// Called when the number of subscribers goes from zero to one (but not from one to two, etc)
const subscription = pipe(
toSource(store),
// ....
subscribe(update => {
_markStoreUpdate(update);
store.set(update);
})
)
// Propagate changes
const unsubscribe = store.subscribe(set)
// Cleanup when there are no more subscribers
return () => {
subscription.unsubscribe()
unsubscribe()
}
})
// Maybe expose some additional properties
return {
set: store.set,
update: store.update,
subscribe,
}
}
// Only handles component lifecycle stuff
export function query(store): Writable<OperationStore> {
// Activate component subscriber to update non-reactive store properties
const unsubscribe = autoquery(store, getClient()).subscribe(noop)
onDestroy(unsubscribe);
return store;
} If I understand you correctly the RFC would ensure that all subscribers get the same data, right? Would something like |
Actually, you do need to write things like I'm not quite sure what you're trying to show with |
My apologies for not being clear enough.
That is my point and the difference between On a personal note: I think the property shortcut access brings more harm than it may be useful because non-svelte reactive prefix access kinda works but is not updated. Enforcing the use of
The autoquery would allow to use the query subscription logic outside of a component. The urql subscription would be managed by svelte store if at least one svelte subscriber is subscribed (see #1527 (comment)).
That is what prefetch is for.
I think this can be implemented by extracting the non-component stuff from |
I would love to be as advanced as you @sastan 👍 |
Summary
This change supports the use of the svelte bindings outside of a component.
For example with @sveltejs/kit it is possible to use the load method to get data before the component is initialized:
Set of changes
@urql/svelte
: addingclient
as last optional argument toquery
,subscription
andmutation
(not a breaking change – API compatible to previous versions)