Unexpected Actions dependency cross-contamination
Hello! I found some problems with actions and I would like to discuss them.
This may be an issue from my part, but due to the time I spent on these problems I'm very confident this has something weird.
It's about how actions' dependencies cross-contaminate other actions' result values. I want to highlight 2 noticeable problems.
Here, I made the an MRE some submissions and targeted effects:
I have two buttons, one that triggers the text action, and the other one triggers the number action. Simple enough.
The text button completes successfully. The second button errors.
Now, regarding the effects, the first one should only care whenever the text action successfully returns.
Nope! Not at all. When I click the text button, that effect triggers twice.
I figured out that it was because its
pending
's state changes, and that triggers the effect, which I explicitly stated I don't care about (with on
).
So, it's like that effect is listening for every fetchedText
property; as if I did on(() => fetchedText, () => ...
(without .result
). But that doesn't work which is expected.12 Replies
To top it all off, both of these actions are intermingled. When I press the text button, the second effect activates, which is unexpected, and leads to very weird behaviors as you add more actions, and other helpers, like
query
when fetching.
And, when pressing the number button, it doesn't trigger the first effect. So the cross-contamination happens from the text action to the number action, but not vice versa, which is even weirder!
I've tried to play with roots, owners, using any primitive I'd think it'd solve the problem, like createSubRoot
, but the end result is the same: deeply intermingled dependencies no matter how you twist it.
Here's a video that quickly demonstrates what I'm saying, and a sandbox: https://stackblitz.com/edit/solidjs-templates-r4uwy7rs?file=src%2FResults.tsxYou can probably drop all the createEffect and Message signals if you just use
useSubmissions
(s at the end) instead which collects all submissions of an action.
https://docs.solidjs.com/solid-router/reference/data-apis/use-submissionsuseSubmissions - Solid Docs
Documentation for SolidJS, the signals-powered UI framework
That is fair. However, I think I didn't get my message across on what my goal was. The message array was just for showcasing the number of submissions that there was happening.
My goal is to make something happen whenever the submission fails or succeeds. Like a triggering a sonner.
It's also not a matter of just checking whichever state I need, whose workaround would involve checking all five properties of the Submission. That is, if I wanted to check if a submission went right, I would need to check that
.pending
is undefined
, .error
is undefined
, etc...)..
For example, if I wanted to clear the previous toast right after the next successful submission, I would need an unneeded workaround since this effect will also be triggered when .pending
changes as well, thus losing the id in the process.
I was just a bit unsure whether this would be better as an Issue in GitHub, or if I was just exaggerating, so that's why I posted this here.As far as I know is that submissions are global and every submission is triggered by any action and then useSubmission will filter if there the action that triggered corresponds to the action passed to the "hook".
What you are trying to do is better done in the event handler instead of the useSubmission with createEffect.
You can also just
const res = await fetchText();
setMessages(prev => […prev, res ]
There was this talk by David Khourshid (Maintainer of XState) about React's useEffects in which he talks about "action effect" and that they do belong inside event handlers https://youtu.be/bGzanfKVFeU?feature=shared&t=645BeJS
YouTube
Goodbye, useEffect - David Khourshid
From fetching data to fighting with imperative APIs, side effects are one of the biggest sources of frustration in web app development. And let’s be honest, putting everything in useEffect hooks doesn’t help much. Thankfully, there is a science (well, math) to side effects, formalized in state machines and statecharts, that can help us visua...
e.g. you could
I see. Now it makes sense
That's a bit annoying since I expected these actions to work where an action result is targeted to a specific submission hook (just like from where this feature was inspired from)
For instance, another example that I forgot to show that happened to me recently is this one:
My intention would be to fetch the list of all names, and whenever an employee was created, listen to that specific submission and then refetch the list name
Of course, this comes as bit of a lazy way to do it, since I can just get back the name of the new employee, insert it into the list, and revalidate the name cache (maybe). But regardless, it's just another simple example to illustrate more my point
Your solutions are fair (and very appreciated btw!), since what you're doing is to just await the action directly without going through the process of using
useSubmission
, or just straight-up fetch the data in the component itself
So basically the workaround is to not use useSubmission
(or even useAction
) at all, since their behaviors are quite not-so-intuitive
The key point here is that submissions become pointless if I can trigger any submission via any action. So really it doesn't matter which action you put in useSubmission
, since it'll trigger all submissions internally, obscurely
So actions become mere fetchers/fetch wrappers since there's not much to them other than that. Therefore this feature becomes redundant
I certainly believe this feature's intention is to have a convenient way to work reactively with async operations in a controlled/localized way, just like createResource
and createAsync
So, at least from my point of view, it would be natural to chuck in an effect listening to the submission hook, just like createAsync
also needs a reactive dependency in order to work properly, which has an isolated scope (as it should)
(if I didn't use this convenient feature, I would just create a handler without needing an effect as you shared)
But again, this is just what I think; I just want to make sure that if what I think is on the right track, or if I'm missing some fundamental concepts/philosophies, which is making me understand this the wrong way, since I don't have an actual really deep insight on this
Although, either way, I'm thinking on making a primitive by trying to play with actions and submissions to make these work exactly as I expect
But due to how they work fundamentally, I might have to reimplement these from scratch, which is a completely different story (that might not be worth looking at)Actions are for data mutations and to revalidate queries.
query
action
go hand in hand.
You can leverage single flight mutations.
I made a video about that:
https://youtu.be/s9G_64eHrfY?si=t3mXBqtbVAQ2h1y-
with useSubmission you can also show optimistic updates.
Actions are made to work without JavaScript in the browser (progressive enhancement)Martin Rapp
YouTube
Actions in SolidStart - How to leverage Single-Flight-Mutations
In this video we'll explore ACTIONS im SolidStart.
You will see how actions invalidate the query cache, how you can pinpoint which cached data should be revalidated an how you can use useSubmission to disable form elements during submission of data.
We will finally explore how we can leverage Single-Flight-Mutations to speed up your app.
Github...
I see
And I just watched your video. Good explanation honestly
I think I should've said that my app is a SPA and it won't use the metaframework
I am aware that, in that case, actions are of not much use then, but they honestly have helped a lot in some things (even the docs say that they are fine to use on client-side things)
But yeah, I'm using them with
query
and all
It's just a bit annoying the fact that the state is global. But I won't complain about its design if avoiding this problem wasn't on the list, but doing the things action
was created for (as you point out). Although I still feel a bit uneasy about this
I might write some primitive hacks to make this work as I want, or I might not since it would require an understanding of topics I can't wrap my head around (yet). But yeah, I don't know
I'll also admit that I'm also a bit of a "use that shiny tool" person (like considering using Modular Forms). That's curiosity for you, I guess
But at least my thoughts are more on track on what I should expect using these features thanks to this thread. So, thank you!
oh well
Okay, at the end I managed to enclose the actions within independent reactive scopes with an undocumented property actions have, and by also providing context to them individually (via owner injection)
I could create the action definition inside each component, but that's pretty much a no-go from an organization perspective
Also, I had a question out of curiosity...
@Madaxen86 Do you know how actions' strings are useful or how can I use them? (i.e.
action(fn: () => Promise<...>, name?: string)
: the second argument)
I know query
uses them for cache purposes (giving each an id), but what about actions? In the docs it says that they are used for debug purposes, but I've never seen them, so I usually don't bother to fill out the second argument
I also saw in your video that you had all of your actions with that string argument filledAfaik They’re relevant for SSR. I think they are used under the hood for hydration and probably for single fight mutations. But I don’t know for sure.
Maybe you could use a factory function.
Don’t know if it would help like
ts
export const initAction = () => action(async () => …,"mutateUser")
And just call that inside your component!?Oh, I see. Thanks!
Yeah, that's the solution basically
I just have to make sure that I don't lose the context if I wanna interact with anything about the component when the action completes