T
TanStack3y ago
stormy-gold

structural sharing do not preserve granularity

using a mutation to update a value in an object force the query to update the full object and not just the singular property. I was wondering if this is a bug or there is a way to preserve the granular updates I made a reproducible stackblitz: https://stackblitz.com/edit/solidjs-templates-xkf75n?file=src/App.tsx
StackBlitz
Solidjs - TanStack query - StackBlitz
Vite + solid templates
36 Replies
harsh-harlequin
harsh-harlequin3y ago
what do you mean with "granularity" ? Can you describe what you'd expect to happen in that example vs. what you are seeing?
stormy-gold
stormy-goldOP3y ago
basically as you can see in the example I linked, I have an object with multiple properties, when I call the mutation I just change one tho ( luckyNumber), despite that, the createEffect with other properties (such as Name, surname...) get triggered, in solidJs it should get triggered only the one you change, so in this case luckyNumber I don't know if you got the point
harsh-harlequin
harsh-harlequin3y ago
so in this case, query.data has to be a new object, because something inside it changed. name, surname, age etc. are all primitives, so they will be equal anyways. Not sure why solid would trigger an effect if you depend on query.data?.name ? In React, this wouldn't re-run unless name really changed:
React.useEffect(
() => console.log(query.data?.name),
[query.data?.name]
)
React.useEffect(
() => console.log(query.data?.name),
[query.data?.name]
)
where structural sharing comes in handy is if you have nested objects, like:
{
names: {
name: 'jhon',
surname: 'smith',
},
age: 30,
luckyNumber: Math.random(),
}
{
names: {
name: 'jhon',
surname: 'smith',
},
age: 30,
luckyNumber: Math.random(),
}
now even though data.luckyNumber is changing, and data is a new reference, data.names will be the same reference.
stormy-gold
stormy-goldOP3y ago
yeah but for some reason it get retriggered, not sure why
harsh-harlequin
harsh-harlequin3y ago
yeah I don't think there's much we can do on the structural sharing side of things
rival-black
rival-black3y ago
@ryansolid @Alex Lohr any ideas on what we could do?
exotic-emerald
exotic-emerald3y ago
Hi, there! I'm not sure, do you already use the storage option in createResource internally? Ah, you're not using createResource only in the BaseQuery, without options.
harsh-harlequin
harsh-harlequin3y ago
structual sharing is a core feature, so it does the same thing in react and in solid
exotic-emerald
exotic-emerald3y ago
Can you show me the part of the code that handles this? Ah, found it already.
rival-black
rival-black3y ago
Right now we are solving this by wrapping the data result in a store and running the create effect on it which updates the store every time, but we have to do this all over the place and it’s not ideal I would have hoped it was possible to abstract it directly within solid query
exotic-emerald
exotic-emerald3y ago
It would really help if you could expose the createResource options so you could make use of the storage option. Should I do a PR?
rival-black
rival-black3y ago
How would that work in user land code? How would it look like?
exotic-emerald
exotic-emerald3y ago
function createDeepSignal<T>(value: T): Signal<T> {
const [store, setStore] = createStore({
value
});
return [
() => store.value,
(v: T) => {
const unwrapped = unwrap(store.value);
typeof v === "function" && (v = v(unwrapped));
setStore("value", reconcile(v));
return store.value;
}
] as Signal<T>;
}

const queryClient = new QueryClient({
defaultOptions: {
queries: {
resourceOptions: {
storage: createDeepSignal() // See at the end of the chapter: https://www.solidjs.com/docs/latest/api#createresource
},
},
},
});
function createDeepSignal<T>(value: T): Signal<T> {
const [store, setStore] = createStore({
value
});
return [
() => store.value,
(v: T) => {
const unwrapped = unwrap(store.value);
typeof v === "function" && (v = v(unwrapped));
setStore("value", reconcile(v));
return store.value;
}
] as Signal<T>;
}

const queryClient = new QueryClient({
defaultOptions: {
queries: {
resourceOptions: {
storage: createDeepSignal() // See at the end of the chapter: https://www.solidjs.com/docs/latest/api#createresource
},
},
},
});
rival-black
rival-black3y ago
But I don’t understand, wouldn’t that be equivalent to structuralSharing with the reconciler, like in our example?
exotic-emerald
exotic-emerald3y ago
Unfortunately, the result is still fed to a signal, which then updates all effects subscribed to it. Since resources don't have an equality comparison, it is run in any case.
rival-black
rival-black3y ago
Oh I see
exotic-emerald
exotic-emerald3y ago
@TkDodo 🔮, I'm happy to create a PR to tanstack/solid-query to expose the resource options later in the evening, if you want it.
harsh-harlequin
harsh-harlequin3y ago
gonna ping @Aryan , he's the expert 🙂
vicious-gold
vicious-gold3y ago
Or rather only a top level equality check so every change would get past the equality check I think given how all in Tanstack Query is I'd just use the Store internally and maybe not expose this createResource is purposely lower level
exotic-emerald
exotic-emerald3y ago
But you only need the storage option if structural shaping is enabled.
fair-rose
fair-rose3y ago
Thank you Alex! Yeah this is a little tricky. We have this issue fixed in v5! The same example works as expected here https://stackblitz.com/edit/solidjs-templates-vacd3v?file=src%2FApp.tsx. I'll try to update v4 to work in a similar fashion so that it doesn't cause undesired behavior.
fair-rose
fair-rose3y ago
I have my hands full at work this Friday. But I'll try to fix it over the weekend. Also if anyone else wants to take a stab at it. Please feel free to open a PR in the meantime 😄 The fixed version is on the alpha branch for reference This approach would be a little more difficult to implement
const queryClient = new QueryClient({
defaultOptions: {
queries: {
resourceOptions: {
storage: createDeepSignal() // See at the end of the chapter: https://www.solidjs.com/docs/latest/api#createresource
},
},
},
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
resourceOptions: {
storage: createDeepSignal() // See at the end of the chapter: https://www.solidjs.com/docs/latest/api#createresource
},
},
},
});
The QueryClient is the main framework agnostic part of TanStack Query. This same client is used between multiple frameworks like vue react svelte and solid. So Adding a resourceOptions to QueryClient would not be ideal since it won't make sense for other frameworks. What we could do is wrap the QueryClient exported by solid query to take in resourceOptions but that would require a lot more work. Alternatively what I was thinking, I dont know how familiar you are with the Tanstack query codebase Ryan 😅 But would it make more sense to use the from primitive (https://www.solidjs.com/docs/latest/api#from) in solid-js instead of a store here https://github.com/TanStack/query/blob/alpha/packages/solid-query/src/createBaseQuery.ts#L51-L53. This would allow us to hand the structuralSharing capabilities back to TanStack Query and we wont need to use the reconcile hack anymore. The referential equality will be maintained by Tanstack query and nothing would need to be done in solid-js land anymore. I'll try to work on an example to make this point clearer
vicious-gold
vicious-gold3y ago
I'm not sure that will work. We don't diff in our framework so structural sharing doesn't really matter Like the problem is that the root signal changes which means everything else will change.
exotic-emerald
exotic-emerald3y ago
If I understand correctly, query-solid can access all the options, thus checking if structuralSharing is intended should be trivial. So instead, we can simply prime the resource with the correct options depending on the use case. I'm not too deep into your codebase (yet), but I maintain the resource-related packages in solid-primitives, so I feel mostly at home.
harsh-harlequin
harsh-harlequin3y ago
if solid doesn't need structuralSharing, I suggest you remove that option from createQuery and pass it as false to the QueryObserver. It does have a runtime overhead.
exotic-emerald
exotic-emerald3y ago
There's not a 100% overlap of structuralSharing and using a deep signal as storage for the resource, since solid's reactivitiy is based on signals rather than immutability. We can also make deep signal a default and remove the structuralSharing. It should have a minimal performance impact for those cases that don't need it. There's an issue with using a store resource; you can no longer rely on using the resource directly to trigger effects. I may have a solution, though, but I need to test it thoroughly. Unfortunately, an update signal breaks all kinds of other reactive assumptions. So this is definitely something we want only to trigger in certain cases.
exotic-emerald
exotic-emerald3y ago
GitHub
GitHub - atk/query at atk-fine-grained-reactive-resource
🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query. - GitHub - atk/query at atk-fine-grained...
exotic-emerald
exotic-emerald3y ago
Infinite queries, relying on tracking the resource base, will fail (and a few others that do the same, too). There are two options to go from here: fix all the other places not to rely on the resource base being reactive (e.g. using the @solid-primitives/deep package) or try to fix the structuralSharing in a way that uses a store instead of a signal to allow for fine-grained reactivity from solid.
fair-rose
fair-rose3y ago
After your feedback and @Alex Lohr helpful comments I think I have fine grained Query Data updates working! I have an example created on my dev package and here is the stackblitz showcasing it. https://stackblitz.com/edit/github-ubelji?file=src%2FApp.tsx I have exposed a reconcile option on createQuery and QueryClient options. The reconcile option can take the following signature
Aryan Deora
StackBlitz
Solidjs - Templates - StackBlitz
Vite + solid templates
No description
fair-rose
fair-rose3y ago
The reconcile option defaults to the id key, to reconcile data updates similar to solid's defaults. It can also take any other reconciliation key as a string. Or if the user wants more control they can provide their own callback and do the reconciliation themselves 😄 with helpers like produce . Or it can be set to false to avoid any reconciliation
fair-rose
fair-rose3y ago
fair-rose
fair-rose3y ago
Surprisingly! The reconcile fn is way more performant than structuralSharing function 🤯 I was able to load 250 distinct query observers without seeing any performance hits!
vicious-gold
vicious-gold3y ago
It isn't surprising. This is why we wanted to move to reactive core. Unfortunately still need to support React. And getting back to structural sharing from here is even more expensive.
fair-rose
fair-rose3y ago
That's amazing! I was struggling to mount 100 different queries in one component before. Now it just takes it like a champ hahaha. Thank you so much for jumping on this and helping out
fair-rose
fair-rose3y ago
https://github.com/TanStack/query/pull/5287 Here is the PR/RFC of the proposed changes that brings fine grained updates to query updates! Any feedback, comment, concern is greatly appreciated 🙏
GitHub
feat(solid-query): Add reconcile option by ardeora · Pull Request...
This pull request proposes a new feature for solid-query that allows for more precise updates to query data. The new reconcile option enables updates to be made between state updates, while the str...
rival-black
rival-black3y ago
I’ll test it as soon as I have some time available, thank you so much everyone for the time you took to get this PR ready so fast!

Did you find this page helpful?