S
SolidJS5mo ago
J

Creating a store with data returned from an async function

Simplifying greatly, this is what I would like to achieve:
const [tab, setTab] = createStore({
path: await homeDir(),
view: await userPreferences.get("preferredView")
});
const [tab, setTab] = createStore({
path: await homeDir(),
view: await userPreferences.get("preferredView")
});
I understand there is no way to get the return result of an async function in top-level code (this is within a .ts file). Now I know some of you might suggest resources, however, I only need to use these async functions at app launch (aka once and never again), and they are all extremely fast (<10ms) so I do not need to use Suspense or add a loading indicator to my GUI. The only solution I can think of at the moment is initializing properties like path and view with "dummy" values and then within an async-friendly initialization point such as onMount, populating said properties with their real values fetched from the async functions. Using dummy values is a really crappy solution in my opinion so I would be so grateful if somebody had a neat solution to this issue. Coming from lower-level languages like Rust, the lack of control over synchronization drives me insane in these frontend frameworks. Thanks.
75 Replies
Brendonovich
Brendonovich5mo ago
You can use createResource and just not add Suspense, I'm pretty sure that will just stop anything from being shown until they're done.
Coming from lower-level languages like Rust, the lack of control over synchronization drives me insane in these frontend frameworks
To be clear, with Solid you have pretty much complete control over how you synchronize your data and UI. You're absolutely able to do homeDir().then(dir => setDirSignal(dir)) in onMount or something.
J
JOP5mo ago
You can use createResource and just not add Suspense, I'm pretty sure that will just stop anything from being shown until they're done.
From the docs:
data() is undefined until fetchData finishes resolving.
So I imagine I would end up getting a ReferenceError using this strategy?
Brendonovich
Brendonovich5mo ago
Since you want to use those values to populate a store, I think I'd fetch them in createResource in a component above the createStore, and then use <Show> so that createStore isn't called until everything is loaded
function Wrapper() {
const data = createResource(() => ...);

return (
<Show when={data()}>
{data => <Inner initialData={data()}/>}
</Show>
)
}

function Inner(props) {
const [tab, setTab] = createStore(props.initialData)
}
function Wrapper() {
const data = createResource(() => ...);

return (
<Show when={data()}>
{data => <Inner initialData={data()}/>}
</Show>
)
}

function Inner(props) {
const [tab, setTab] = createStore(props.initialData)
}
J
JOP5mo ago
I kind of want to isolate this data into a .ts file as it's global app data that will be used absolutely everywhere, including in functions in other .ts files You'll have to bear with me as I am coming from Svelte after not working with JSX or React in 2 years (with barely any experience even back then).
Brendonovich
Brendonovich5mo ago
Do you want homeDir or tab available globally?
J
JOP5mo ago
I'll just provide you with a better simplified schema of the global state I want as my first example was not robust enough: src/app/state/index.ts:
const [tabs, setTabs] = createStore([
{
path: await homeDir()
// ...Some other properties that may be loaded async such as data from disk
}
])
const [tabs, setTabs] = createStore([
{
path: await homeDir()
// ...Some other properties that may be loaded async such as data from disk
}
])
Since I need each tab to be part of this array of tabs, I can't isolate specific properties into resources or signals, or at least, I'm not sure how I would integrate something like a Resource into a store... src/app/index.ts:
// Must be able to be accessed anywhere else in the application and this function must be able to access tabs from src/app/state of course
export function refreshTab(tabIndex: number) {
// Omitted
}
// Must be able to be accessed anywhere else in the application and this function must be able to access tabs from src/app/state of course
export function refreshTab(tabIndex: number) {
// Omitted
}
I'm using Tauri (I see that you're parting of the working group, neat!) and my application is pretty unique for a web framework like solid so it's hard to translate the expected web paradigms described in the docs into something that's usable for my app. I can't really just wrap things in components I need to be able to access this state from everywhere and I of course need it to be reactive.
Brendonovich
Brendonovich5mo ago
I think I'd do the same approach I described above and make tabs accessible via context, refreshTab can then take in the specific tab item as an argument instead of receiving the index and having to look it up itself Context and Suspense worked great for us building Spacedrive
J
JOP5mo ago
Hmm, I'll have to switch up my design paradigm then I really appreciate the help!
Brendonovich
Brendonovich5mo ago
No worries, hope you get it worked out Also, you should be able to integrate resources into stores using getters
const [tabs, setTabs] = createStore([
{
get path() {
return homeDirResource()
},
// ...Some other properties that may be loaded async such as data from disk
}
])
const [tabs, setTabs] = createStore([
{
get path() {
return homeDirResource()
},
// ...Some other properties that may be loaded async such as data from disk
}
])
Ah nvm You can't overwrite path with this setup
J
JOP5mo ago
Yeah One solution I've thought about is using the return value of createResource, then using the accessor and loaded things as usual, and using mutate to change it later But once again using a resource is pretty redundant after initialization
Brendonovich
Brendonovich5mo ago
This implies that the source of truth for path would be the resource, rather than tabs which seems wrong. Most of these sorts of problems can be solved by thinking about whether the source of truth for the data should be If you're gonna be reusing homeDir for new tabs then not really, you need somewhere to store it
J
JOP5mo ago
What do you mean by "source of truth"?
Brendonovich
Brendonovich5mo ago
Like the place that dictates 'this is the values of this thing' With the getter example, even though tabs[0].path exists, the source of truth would be the resource you're mutating Which doesn't make sense since each tab should be the source of truth for its own path
J
JOP5mo ago
I'm not a frontend dev at heart but what I'm hearing is that it's discouraged to wrap data in a Store within a Resource? Actually that does make sense because stores have their own proxying for reactivity
Brendonovich
Brendonovich5mo ago
I wouldn't say discouraged, rather it probably doesn't make sense to do so in a lot of cases And source of truths are relevant for the backend too, thinking about whether the source of truth should be some database, a piece of Tauri state, a value local to a function scope, or a remote machine
J
JOP5mo ago
The fact that something as simple as using async code to initialize values in a store is such a complex thing in any framework is one of the reasons I really dislike doing frontend work :'/ So you could describe source of truth as just the true origin of the data?
Brendonovich
Brendonovich5mo ago
Yeah sure It's important to consider because down the line you're going to be doing transformations and derivations on that data, and if a modification is important you want to be applying it to the actual source of truth rather than some copy
J
JOP5mo ago
Also I guess I meant the ambiguity around synchronization that comes with using frontend frameworks. In languages like Rust, it's very clear what order code takes place (ignoring async ofc but that's a whole nother can of worms).
Brendonovich
Brendonovich5mo ago
Doing something like let a = reqwest::get().await; is pretty much the same as the <Show> example I mentioned, since you're suspending the current future until the data resolves And if you wanted global state that came from an async source in Rust I feel like you'd have similar synchronization issues Like do you put it in a global static with OnceCell or Lazy, or a thread local, or have an optional field on a global struct and a task that receives from a channel and then updates the optional field, or no global at all and instead passing around some struct that has the prefetched data via function arguments
J
JOP5mo ago
Well that is one hell of a sentence my friend
Brendonovich
Brendonovich5mo ago
added some commas haha
J
JOP5mo ago
At this point I think I'm having bigger issues because I am used to having a pretty clear separation between data and the gui code that reacts to that data
Brendonovich
Brendonovich5mo ago
i mean what do you usually use?
J
JOP5mo ago
Whether that's the right or wrong approach I may have to figure out Svelte I had this same issue with using async when creating stores and I ended up using the dummy data method I mean it did its job and the app works fine But it's not ideal and now that I'm trying out Solid to see if a rewrite in it would be beneficial I want to be as idiomatic as possible to avoid a bunch of refactoring pain in the future Biggest pain in the ass in svelte was the lacked of nested reactivity So you can imagine solid became pretty attractive
Brendonovich
Brendonovich5mo ago
From how I see it, you can't get around the fact that you either a) don't create the list of tabs until you have loaded the prerequisite data, or b) use that dummy data until you have loaded the proper data. It'll happen in both Svelte and Solid and is just par for the course of dealing with async data sources Having used svelte I also agree Solid is nicer haha
J
JOP5mo ago
Yeah I'm hoping Solid is like Rust in that the DX and performance were given very deep thought There's a lot of holes in Svelte when you do anything past the basics and that really contrasted with Rust. Writing the backend was an absolute pleasure, but then came the frontend logic and it was just horrible in comparison. Thanks, I needed the reassurance that there wasn't a better way, I'll choose one or the other.
Brendonovich
Brendonovich5mo ago
Performance is absolutely a top priority, DX I'd say depends on what your're talking about. Solid provides a lot of primitives that let you build the way you want which could be a better DX if that's what you're into, but could also be harder if you're looking for a bit more hand holding
J
JOP5mo ago
By great DX I mean this:
if let Ok(Some(val)) = result {
...
}
if let Ok(Some(val)) = result {
...
}
It's very rare I find myself writing what feels like redundant code in Rust (plus macros can supplant that a lot of the time) Lmao Yeah I'm willing to sacrifice ease of use for performance, we'll see how it goes
Brendonovich
Brendonovich5mo ago
How's this for you haha
<ErrorBoundary>
<Show when={result()}>
{val => ...}
</Show>
</ErrorBoundary>
<ErrorBoundary>
<Show when={result()}>
{val => ...}
</Show>
</ErrorBoundary>
J
JOP5mo ago
Yeah I need some time to wrap my head around the JSX way of doing this after using Svelte for so long Anyways I really appreciate your help, made a big difference after dredging through the docs for so long. Have a good one @Brendonovich Another question, would it be possible to have a reactive Solid store that is also bidirectionally synced with a Tauri disk store?
createEffect(() => {
for (const [key, value] of Object.entries(solidStore))
tauriStore.set(key, value);
});
this.diskStore.onChange((key, value) => {
// @ts-ignore
setSolidStore(key, value);
});
createEffect(() => {
for (const [key, value] of Object.entries(solidStore))
tauriStore.set(key, value);
});
this.diskStore.onChange((key, value) => {
// @ts-ignore
setSolidStore(key, value);
});
The code I prototyped above looks like it would produce an infinite loop...
Brendonovich
Brendonovich5mo ago
if tauriStore.set triggers diskStore.onChange then yeah that'll cause an infinite loop haha Is the source of truth in this case the Tauri store, and the UI is just a reflection of it, or the UI store, where the Tauri store is just a backup? Bc in the latter case I think I have an easy solution for you
J
JOP5mo ago
Sorry, source of truth is for sure the Tauri store Reason being I need to share this state between windows and have it automatically update the UI
Brendonovich
Brendonovich5mo ago
In that case I don't think i'd have the createEffect I think i'd directly update the tauri store, and have the UI store be updated later by diskStore.onChange
J
JOP5mo ago
How would I subscribe to changes on the Solid store then?
Brendonovich
Brendonovich5mo ago
And if you wanted the UI to update immediately you could write directly to the store, which would then get overwritten by diskStore.onChange Basically the same as doing optimistic updates with an http api
J
JOP5mo ago
I don't really want to manually write setters for every property My hope was that I could create a catch-all that would avoid boilerplate
Brendonovich
Brendonovich5mo ago
Wym setters for every property? You already have to write bindings for each field to the UI
J
JOP5mo ago
Yeah, I'd like to be able to just assign a property in the Solid store and have that change reflected in the Tauri store Not sure how binding works in Solid, haven't looked into that yet give me a minute to check the docs
Brendonovich
Brendonovich5mo ago
By bindings i mean onInput handlers and stuff
J
JOP5mo ago
Is there no equivalent to something like svelte's bind:value on an input?
Brendonovich
Brendonovich5mo ago
Nah
J
JOP5mo ago
Shit
Brendonovich
Brendonovich5mo ago
Solid is simple, not easy lol
J
JOP5mo ago
What's the reasoning behind that?
Brendonovich
Brendonovich5mo ago
GitHub
Proposal: consider using 2-way bindable structures. · solidjs solid...
I know that, this might sound as a paradigm change, however inspired by this library, I've created a poc library for 2-way bindable structures, that might be considered for solid: This would ch...
Brendonovich
Brendonovich5mo ago
one-directional data flow > two way bindings in solid land
J
JOP5mo ago
Yeah that figures I've had some nasty bugs arise from two way flow
Brendonovich
Brendonovich5mo ago
Here's something that might work
import { makePersisted, tauriStorage } from "@solid-primitives/storage"
import { Store } from "@tauri-apps/plugin-store"

const store = new Store(NAME)

const [state, setState] = makePersisted(
createStore(),
{ storage: tauriStorage(NAME) }
);

store.onChange((key, value) => {
setState(key, value)
})
import { makePersisted, tauriStorage } from "@solid-primitives/storage"
import { Store } from "@tauri-apps/plugin-store"

const store = new Store(NAME)

const [state, setState] = makePersisted(
createStore(),
{ storage: tauriStorage(NAME) }
);

store.onChange((key, value) => {
setState(key, value)
})
J
JOP5mo ago
Only issue with one way is that it won't reflect on another window when it's changed A prime example being 2 windows with a settings page open on each Aha! Fantastic I thought somebody else might have thought of this before
Brendonovich
Brendonovich5mo ago
Sure it will, if a value in one window's store changes then it'll trigger onChange in the other window and update that window's store, which will rerender the UI makePersisted intercepts the setState call rather than using an effect so you don't have the infinite loop problem
J
JOP5mo ago
Not if it's setup like this <input value={store.preference} onInput={(event) => store.preference = event.target.value} />
Brendonovich
Brendonovich5mo ago
I'd just watch out for synchronisation issues, one window's state will update instantly while the others lag behind sure it will
J
JOP5mo ago
ah i'm trippin
Brendonovich
Brendonovich5mo ago
also do setStore('preference', event.target.value)
J
JOP5mo ago
yeah ofc that's just my svelte brain talking
Brendonovich
Brendonovich5mo ago
lmao unless you use createMutable
J
JOP5mo ago
I've heard pretty bad things about createMutable lol include makes it easy enough either way
Brendonovich
Brendonovich5mo ago
if your data is a tree it's great otherwise avoid it
J
JOP5mo ago
When wouldn't it be? You mean arrays v objects or sumthn?
Brendonovich
Brendonovich5mo ago
I more just mean highly nested data, which trees tend to be. Passing down the setter and calling it properly can get annoying in those cases Recursive data especially, since you have to design your components to work infinitely nested
J
JOP5mo ago
Wait, avoid include or createMutable?
Brendonovich
Brendonovich5mo ago
createMutable haha
J
JOP5mo ago
Okay good So there's no real downside of just putting in the little extra work required for using a store/signal over createMutable right?
Brendonovich
Brendonovich5mo ago
If you want to avoid the footguns that createMutable can add then yeah
J
JOP5mo ago
It appears that @solid-primites/storage has a sync api but it doesn't seem that they have tauri integrated with it
Brendonovich
Brendonovich5mo ago
import { tauriStorage } from "@solid-primitives/storage/tauri" got it wrong the first time
J
JOP5mo ago
With messageSync, you can recreate the same functionality for other storages within the client using either the post message API or broadcast channel API. If no argument is given, it is using post message, otherwise provide the broadcast channel as argument
I wonder if this works between windows with Tauri... I doubt it
Brendonovich
Brendonovich5mo ago
tauriStorage uses @tauri-apps/plugin-store under the hood
J
JOP5mo ago
I'm aware I was rawdogging it before with svelte
Brendonovich
Brendonovich5mo ago
Oh i thought you'd already implemented the onChange part Oh nvm that does exist
J
JOP5mo ago
I was just wondering if that might cause an infinite loop as well. I don't really know how it works under the hood. Wdym
Brendonovich
Brendonovich5mo ago
Ohh i get u Looks like set will trigger onChange in the same window
J
JOP5mo ago
bummer
Brendonovich
Brendonovich5mo ago
Good thing makePersisted is a wrapper around the setter
import { makePersisted, tauriStorage } from "@solid-primitives/storage"
import { Store } from "@tauri-apps/plugin-store"

const store = new Store(NAME)

const [state, setRawState] = createStore();
const [_, setState] = makePersisted(
[state, setRawState],
{ storage: tauriStorage(NAME) }
);

store.onChange((key, value) => {
setRawState(key, value)
})
import { makePersisted, tauriStorage } from "@solid-primitives/storage"
import { Store } from "@tauri-apps/plugin-store"

const store = new Store(NAME)

const [state, setRawState] = createStore();
const [_, setState] = makePersisted(
[state, setRawState],
{ storage: tauriStorage(NAME) }
);

store.onChange((key, value) => {
setRawState(key, value)
})
J
JOP5mo ago
I'll give that a try later Giving a new framework a try this late at night was a bad idea
Want results from more Discord servers?
Add your server