-
-
Notifications
You must be signed in to change notification settings - Fork 649
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
Is there a plan to make liveQuery more compatible with Svelte5 runes #2075
Comments
Thanks for bringing it up. Also asked on stackoverflow: https://stackoverflow.com/questions/78089371/dexie-livequery-with-svelte-5-runes Need more knowledge about Svelte 5 and runes before I could do anything about it but a PR would be much welcome. Could we introduce some kind of svelte helper library that delivers runes for live queries? A bit like we have for react? |
The recently-added fromStore alongside the wrapper from #1907 worked for me: import { liveQuery } from 'dexie'
import { fromStore } from 'svelte/store'
export const liveQ = q => fromStore({
subscribe: (run, invalidate) =>
liveQuery(q).subscribe(run, invalidate).unsubscribe,
})
// usage
const friends = liveQ(async () => await db.friends.where('age').between(18, 65).toArray())
$inspect(friends.current) |
Hello, I am also trying to use Dexie.js in a Svelte5 project with runes. I started by following the existing tutorial Get started with Dexie in Svelte tutorial. However, I got stuck when trying to make a liveQuery reactive to props/state. I was able to get it working by combining
|
It's correct to recreate the query whenever a closure it depends on changes. In the background the old query will be unsubscribed and a new query subscribed and the result will stay until the new query emits it's initial value. |
We'd need some svelte-specific library to consume live queries more slick with runes. See also my suggested helper in #2089 - it emits errors and loading status but it still need the <script lang="ts">
import { db } from '$lib/store';
import { liveQuery } from 'dexie';
let { namePrefix }: { namePrefix: string } = $props();
const friends = $derived(
liveQuery(
() => db.friends
.where('name')
.startsWith(namePrefix)
.toArray()
)
);
</script>
{#if $friends}
<ul>
{#each $friends as friend (friend.id)}
<li>{friend.name}, {friend.age}</li>
{/each}
</ul>
{/if} Could anyone confirm whether the above code would work? It would be more direct than what's suggested so far. If errors and loading status is requested, then look at how it could be done in #2089. An updated sample using that helper would then be: <script lang="ts">
import { db } from '$lib/store';
import { svelteLiveQuery } from '$lib/svelteLiveQuery'; // or where to put it...
import { liveQuery } from 'dexie';
let { namePrefix }: { namePrefix: string } = $props();
const friendResult = $derived(
svelteLiveQuery(
() => db.friends
.where('name')
.startsWith(namePrefix)
.toArray()
)
);
</script>
<!-- isLoading -->
{#if $friendResult.isLoading}
<p>Loading...</p>
{/if}
<!-- error -->
{#if $friendResult.error}
<p>Error: $friendResult.error</p>
{/if}
<!-- result -->
{#if $friendResult.value}
<ul>
{#each $friendResult.value as friend (friend.id)}
<li>{friend.name}, {friend.age}</li>
{/each}
</ul>
{/if}
|
I've also been struggling with various incantations of this. It works acceptably in certain toy app scenarios, but it's a frustrating nightmare in a production app. I think I've got a working solution now, thanks in large part to standing on the shoulders of @braebo, who posted a solution here (see the "fixed" route). The QueryRune works correctly for simple cases. One gotcha is that it doesn't behave as expected with queries that have derived parameters.So for example, this doesn't work:
( This is not reactive because according to the Svelte docs:
(emphasis mine) Because The least untidy solution I've found is to bring back the concept of ye olde deps array. 😭 I've created a function like this: export function liveRune<T>(
querier: () => T | Promise<T>,
..._dependencies: any[]
): QueryRune<T> | { current: undefined } {
return new QueryRune(liveQuery(querier))
} I can call it like this: const draftQuery = $derived(liveRune(() => getSelectedDraft(book?.id), book?.id)) The function doesn't do anything with the second I've actually extended this function to short circuit if any of the deps are undefined, so cascading loads can behave sanely when waiting for data from a parent query (book, in this example): export function liveRune<T>(
querier: () => T | Promise<T>,
...dependencies: any[]
): QueryRune<T> | { current: undefined } {
if (!dependencies.every((x) => x)) {
return { current: undefined }
}
return new QueryRune(liveQuery(querier))
} Svelte's generally done an excellent job of inferring dependencies, so I'm pretty bummed out that it isn't able to do the right thing without an explicit dependency. But it's the best I've found so far. |
@dusty-phillips thanks for sharing your findings! I'm not sure if this helps, but I've got derived working reliably here. |
In practice, recreating the query when dependent state changes is causing the query to very briefly return Has anyone else seen/resolved this? I'm experimenting with "store the old value in separate state until the next value resolves", but it makes me nervous. Edit: Here's some code that seems to work, but continues to make me nervous: import type { Subscriber, Unsubscriber } from 'svelte/store';
import { untrack } from 'svelte';
import { liveQuery } from 'dexie';
// With thanks to https://github.com/braebo/svelte-5-dexie-test/blob/main/src/lib/QueryRune.svelte.ts
export interface ReadableQuery<T> {
subscribe(
this: void,
run: Subscriber<T>,
invalidate?: () => void
): {
unsubscribe: Unsubscriber;
};
}
export type ReadableValue<T> = T extends ReadableQuery<infer U> ? U : never;
export class QueryRune<T = ReadableValue<ReadableQuery<unknown>>> {
current = $state<T>();
constructor(public readonly store: ReadableQuery<T>) {
untrack(() => {
store
.subscribe((v) => {
this.current = v;
})
.unsubscribe();
});
$effect.pre(() => {
return store.subscribe((v) => {
this.current = v;
}).unsubscribe;
});
}
}
export class DexieQuery<T, A extends any[]> {
#querier: (...args: A) => T | Promise<T>;
#args = $state<A>();
#previous = $state<T>();
#latest: undefined | QueryRune<T | undefined> = $derived(
this.#args === undefined
? undefined
: new QueryRune(
liveQuery(() => (this.#args === undefined ? undefined : this.#querier(...this.#args)))
)
);
current = $derived(this.#latest?.current ?? this.#previous);
constructor(querier: (...args: A) => T | Promise<T>) {
this.#querier = querier;
}
setDependencies(...args: A) {
this.#previous = this.#latest?.current;
this.#args = args;
}
} Aside from making me nervous, I also don't like what it does to callsites: const allFriends = new DexieQuery(() => db.friends.toArray());
allFriends.setDependencies();
let ageFilter = $state(0);
const filteredFriends = new DexieQuery((ageFilter) => {
return db.friends.where('age').above(ageFilter).toArray();
});
$effect(() => filteredFriends.setDependencies(ageFilter)); I can't find a way to hide the Edit 2: Inspired by https://github.com/svecosystem/runed/blob/main/packages/runed/src/lib/utilities/previous/previous.svelte.ts I created the following: export class CachePrevious<T> {
#previous: T | undefined = $state(undefined);
#curr?: T;
constructor(getter: Getter<T>) {
$effect(() => {
if (this.#curr !== undefined) {
this.#previous = this.#curr;
}
this.#curr = getter();
});
}
get current(): T | undefined {
return this.#previous;
}
} This works with the export function liveRune<T>(querier: () => T | Promise<T>, ..._dependencies: any): QueryRune<T> {
return new QueryRune(liveQuery(querier));
} In use, I need three separate values: const filteredFriendsQuery = $derived(
liveRune(() => {
return db.friends.where('age').above(ageFilter).toArray();
}, ageFilter)
);
const filteredFriendsPrevious = new CachePrevious(() => filteredFriendsQuery.current);
const filteredFriends = $derived.by(
() => filteredFriendsQuery.current ?? filteredFriendsPrevious.current
); I don't love it because it's still stuffing an extra copy of the stale data in RAM, but at least I've gotten rid of the |
...the problem is that I also can't sleep when my brain has a puzzle to chew on. This is working for the two toy queries I tried it with: import { untrack } from 'svelte';
import { liveQuery } from 'dexie';
export class DexieQuery<T, A extends any[]> {
#previous = $state<T>();
#store = $state<T>();
current = $derived(this.#store ?? this.#previous);
constructor(querier: (...args: A) => T | Promise<T>, dependencies: () => A) {
$effect(() => {
const deps = dependencies();
untrack(() => {
if (this.#store !== undefined) {
this.#previous = this.#store;
}
const store = liveQuery(() => querier(...deps));
return store.subscribe((value) => {
this.#store = value;
this.#previous = undefined;
}).unsubscribe;
});
});
}
} It's basically a bit of all the previous proposed solutions mixed together. In use it looks like this: const allFriendsQuery = new DexieQuery(
() => db.friends.toArray(),
() => [] as const
);
const allFriends = $derived(allFriendsQuery.current);
const filteredFriendsQuery = new DexieQuery(
(age) => db.friends.where('age').above(age).toArray(),
() => [ageFilter] as const
);
const filteredFriends = $derived(filteredFriendsQuery.current);
$inspect('filteredFriends', filteredFriends); // never undefined once set! The |
Is it required to pass the dependencies to the query? This seems to work for me in basic test: export class DexieQuery<T, A extends unknown[]> {
#previous = $state<T>();
#store = $state<T>();
current = $derived(this.#store ?? this.#previous);
constructor(querier: () => T | Promise<T>, dependencies?: () => A) {
$effect(() => {
dependencies?.();
untrack(() => {
if (this.#store !== undefined) {
this.#previous = this.#store;
}
const store = liveQuery(() => querier());
return store.subscribe((value) => {
this.#store = value;
this.#previous = undefined;
}).unsubscribe;
});
});
}
}
// Usage:
const allFriendsQuery = new DexieQuery(async () => await db.friends.toArray());
const filteredFriendsQuery = new DexieQuery(
async () => await db.friends.where('age').above(age).toArray(),
() => [age]
); In my use-case, I dont ever change back to export function stateQuery<T>(querier: () => T | Promise<T>, dependencies?: () => unknown[]) {
const query = $state<{ result?: T }>({});
$effect(() => {
dependencies?.();
return liveQuery(querier).subscribe((result) => {
if (result !== undefined) {
query.result = result;
}
}).unsubscribe;
});
return { query };
}
// Usage:
let { query: friendsQuery } = stateQuery(
async () => {
return await db.friends.where('age').above(age).toArray();
},
() => [age]
); I also dont bother to use another |
Excellent! To make it even simpler we can return the query directly and not bother with destructuring: export function stateQuery<T>(
querier: () => T | Promise<T>,
dependencies: () => unknown[]
): { current?: T } {
const query = $state<{ current?: T }>({ current: undefined });
$effect(() => {
dependencies?.();
return liveQuery(querier).subscribe((result) => {
if (result !== undefined) {
query.current = result;
}
}).unsubscribe;
});
return query;
} let filteredFriendsQuery = stateQuery(
() => db.friends.where('age').above(ageFilter).toArray(),
() => [ageFilter]
);
const filteredFriends = $derived(filteredFriendsQuery.current);
$inspect('filteredFriends', filteredFriends); // never undefined once set! |
Seems you have found something useful here ! Not sure I grip everything yet, but would it be doable to put this into a library? I would love to update the svelte docs when we have something that seems to be a good solution for svelte runes. |
I can try but no promises on timing as I’m pretty swamped! Would you like a PR for a new dexie.js package a la react-live-query, or a third party solution? |
I'm open to whatever would feel comfortable for you - if you'd prefer owning it I'm happy. I you rathe want to hand it over, a recursive copy of libs/dexie-react-hooks would be a good start - remove its react deps, code and tests and put in the deps and code needed for svelte. That would be a nice start as a PR. |
I had some time today so I've made a pull request, let me know if there are any issues you might like me to address. |
I tried to use the workaround solution, please advise me better solution. source code in. |
With the introduction of runes, store is kinda obsolete. Is there any plan to support runes once svelte5 released?
I use it like this in svelte5
The text was updated successfully, but these errors were encountered: