-
-
Notifications
You must be signed in to change notification settings - Fork 646
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
Create a library for a reactive Dexie.js query in Svelte 5 #2116
base: master
Are you sure you want to change the base?
Conversation
Thanks! The reason it fails to build seem to be outside of this PR - something has become broken elsewhere. |
what about this little supplementary helper function? export function liveQueryState<T, I>(
querier: () => T | Promise<T | undefined>,
dependencies: () => unknown[] = () => [],
default_state: I
): T | I {
return $derived(stateQuery(querier, dependencies).current || default_state);
} with usage: let friendsQuery = liveQueryState(
() => db.friends.where('age').between(minAge, maxAge).toArray(),
() => [minAge, maxAge],
[{name: 'Jesus' age: -Infinity}]
); It returns what svelte users would expect from a svelte version of liveQuery. I added the You could add a export function stateQuery<T, I>(
querier: () => T | Promise<T | undefined>,
default_state: I,
dependencies: () => unknown[] = () => []
): { current: T | I } {
const query = $state<{ current: T | I }>({ current: default_state });
$effect(() => {
dependencies?.();
return liveQuery(querier).subscribe((result: T | undefined) => {
if (result === undefined) {
query.current = default_state;
} else {
query.current = result as T;
}
}).unsubscribe;
});
return query;
} |
Part of the issue was that rendered components would flicker very briefly when the dependencies changed. Your example would have the same issue of flickering, but to a default value instead. I think that a better approach for your use-case would be to not have the check for const friendsQuery = liveQueryState(
() => db.friends.where('age').between(minAge, maxAge).toArray(),
() => [minAge, maxAge]
);
let friends = $derived(friendsQuery.current ?? [{name: 'Jesus', age: -Infinity}]); Keeping the |
I was also looking at Issue #2089 and considered something along the lines of: export function stateQuery<T>(querier: () => T | Promise<T>, dependencies?: () => unknown[]) {
const query = $state<{ result?: T; isLoading: boolean; error?: any }>({
result: undefined,
isLoading: true,
error: undefined,
});
$effect(() => {
dependencies?.();
return liveQuery(querier).subscribe(
(result) => {
query.error = undefined;
if (result !== undefined) {
query.result = result;
query.isLoading = false;
} else {
query.isLoading = true;
}
},
(error) => {
query.error = error;
query.isLoading = false;
}
).unsubscribe;
});
return query;
} And then you could achieve a default state with: const friendsQuery = stateQuery(
() => db.friends.where('age').between(minAge, maxAge).toArray(),
() => [minAge, maxAge]
);
let friends = $derived(
friendsQuery.isLoading || friendsQuery.result === undefined
? [{ name: 'Jesus', age: -Infinity }]
: friendsQuery.result
); |
Its annoying that it cant be used with a single line but i guess that's down to the constraints of but as its can't your |
Is it just an initial state that you're looking for, or for any time it is loading? Svelte's Why is your default state a different type to the query result type? If it is intentionally different, then I think it should be separated into a I was trying to stick as close to the export function stateQuery<T>(querier: () => T | Promise<T>, dependencies?: () => unknown[], initialValue?: T) {
const query = $state<{ value?: T; isLoading: boolean; error?: any }>({
value: initialValue,
isLoading: true,
error: undefined,
});
...
const friendsQuery = stateQuery(
() => db.friends.where('age').between(minAge, maxAge).toArray(),
() => [minAge, maxAge],
[{ name: 'Jesus', age: -Infinity }]
); |
having a different initial and loading type is an edge case and, as you say, it could be implemented with I see you suggested renaming I started on this journey because I wanted to upgrade from svelte 4 to svelte 5. I previously was using the pattern: /// $lib/components/things.ts
...
let search_input = writable('');
$: things = query_centre.searchThings($search_input);
...
/// $lib/query_centre.ts
class QueryCentre {
searchThings(query: string) {
/// do stuff to find things and update db...
return liveQuery(async () => {
return await db.repos.where('searchWords')
.startsWithAnyOfIgnoreCase(query)
.distinct()
.toArray();
});
}
const query_centre = new QueryCentre();
export default query_centre; but with svelte 5 I wanted to do this: /// $lib/components/things.ts
...
let search_input = $state('');
let things: T | undefined = $derived(query_centre.searchThings(search_input));
...
/// $lib/ query_centre.ts
class QueryCentre {
searchThings(query: string) {
/// do stuff to find things and update db...
return liveQueryState(
() => db.repos.where('searchWords').startsWithAnyOfIgnoreCase(query).distinct().toArray(),
() => [query], // I now realize this is unnecessary
});
}
const query_centre = new QueryCentre();
export default query_centre; but I dont think liveQueryState can be implemented to work like that. I could use your function as is but having to define /// $lib/components/things.ts
...
let search_input = $state('');
let things_query = $derived(query_centre.searchThings(search_input));
let things = $derived(things_query.current || []);
...
}
/// $lib/ query_centre.ts
class QueryCentre {
searchThings(query: string) {
/// do stuff to find things and update db...
return liveQueryState(
() => db.repos.where('searchWords').startsWithAnyOfIgnoreCase(query).distinct().toArray(),
});
} or just leave /// $lib/components/things.ts
...
let search_input = $state('');
let things = $derived(query_centre.searchThings(search_input));
...
} Would you say using your function would be the most idiomatic use of svelte 5? EDIT: I'm still getting the flickering with both these svelte 5 patterns. |
The renaming is accidental. For me, it makes sense to have the "result" of a query and that is what I have in my project code. I see "current" being used by others and in other places, but for me it doesn't quite feel right here. When I issue a new query that has not returned yet, the value is no longer "current" but it is now an "old" value. I think "value" would probably be a good alternative, but I will continue to use "result" in my own project, so I'm happy for this to be named whatever makes the most sense for others. I tend to only use "current" for things like dynamic programming with lists/queues/graphs. I think the reason that you are still getting flickering is because the $derived function re-runs the searchThings function that creates the query entirely, you're receiving a new state each time rather than updating the state, which is also why the dependencies are unnecessary. Try removing the “$derived” function wrapper in your first Svelte 5 example, I think that’s will be what you are looking for. I’m away and can’t test or give code examples currently. Since liveQuery returns an observable, it is the actual resulting value from the query, and anything that subscribes to the observable will get updated. However with signals, used in Svelte 5, changes to the value itself will not cross the function boundary, hence why we need a property in the $state object, which will propagate changes. |
This is just a proof of concept and can be improved, but I think this is more what you're trying to do: /// $lib/query_center.svelte.ts
export class QueryCenter {
search = $state('');
query = $state<{ value?: unknown; isLoading: boolean; error?: unknown }>({
value: undefined,
isLoading: true,
error: undefined
});
constructor(db: Dexie) {
$effect(() => {
(() => [this.search])(); // no-op so it reloads
return liveQuery(() =>
db.repos.where('searchWords').startsWithAnyOfIgnoreCase(search).distinct().toArray()
).subscribe(
(value) => {
this.query.error = undefined;
if (value !== undefined) {
this.query.value = value;
this.query.isLoading = false;
} else {
this.query.isLoading = true;
}
},
(error) => {
this.query.error = error;
this.query.value = undefined;
this.query.isLoading = false;
}
).unsubscribe;
});
}
} And now you can use it with less boilerplate: /// $lib/Component.svelte
...
const queryCenter = new QueryCenter(db);
...
<input bind:value={queryCenter.search} />
{#each queryCenter.query.value ?? [] as repo (repo.id)}
... |
I put the code from issue #2075 into a Svelte library.
I just used
sv create
and stripped it down a bit.