[Bug?]: Transition makes it impossible to show loading screen for long-running backend processes
Duplicates
- [x] I have searched the existing issues
Latest version
- [x] I have tested the latest version
Context 🔦
I have a very long-running process on the backend (it involves starting up a browser at an arbitrary URL and collecting some data from the page, then asking an LLM about it) which takes roughly 10-30s.
I would like to keep using the super-simple backend/frontend integration that solid start provides (which blew my mind). Right now it works great in the initial pageload, however, when the user changes their input, solid's router inadvertently starts a transition around my app, which leads to my loading screen not being displayed.
A workaround is to also displaying my loading screen when useIsRouting() is true, but my loading screen shows the user exactly what steps is in progress and therefore I need to be able to know which createAsync/createResource is loading.
Due to the cursed transition this is impossible. I can't use individual <Suspense>s nor can I use .loading of a resource, as everything is lying during a transition.
Steps to reproduce 🕹
Steps:
- Go to https://stackblitz.com/edit/github-3j8b6an5-tc2z1ort?file=src%2Froutes%2Findex.tsx using a chromium based browser
- Wait for "We are loading…" to complete
- Enter something into the input field and press submit
Current behavior 😯
After the user presses "submit", NOTHING happens until the result is displayed after 10s.
Expected behavior 🤔
"We are loading…" is displayed again until the next response shows up
Yet unanswered thread in discord: https://discord.com/channels/722131463138705510/910635844119982080/1353843298019835986
Question was answered on discord, pasting answers for SEO of future people having this issue:
𖠰🌲 — Yesterday at 21:46 How can I stop latest solid-start's router from "transitioning" when I change a query param? Right now it freezes my whole app from updating. Even if I nest a Suspense inside my route around everything, that suspense fallback isn't shown. My backend takes 20 seconds to respond and I NEED to show a loading screen but right now the built in transitioning that seems unbreakable stops me from doing that I've had issues with transitions since 2023 reactivity reactivity Also, something like playgorund.solidjs.com but for solid start would be nice to share reproductions Jasmin — Yesterday at 21:54 Stackblitz is a good way https://stackblitz.com/github/solidjs/solid-start/tree/main/examples/bare 𖠰🌲 — Yesterday at 21:56 Perfect, TYSM! Okay so it seems like solid doesn't freeze the whole app in a transition, just the returnvalues of createAsync, so I can use const isRouting = useIsRouting();to trigger my loading screen 🎉 Why is there no .loadingof the createAsyncthough that I can use to know specifically which one is loading? Because things are so slow that the user needs to be informed about the steps of progress zulu — Yesterday at 22:05 it is odd, that createAsync does not have .loading it is surprising that it has a limited capabilities of the createResource that it is based on I think in the 2.0 the "capabilities" will be importable import {isLoading, isPending, etc } from "solidjs which you can use to test the state of the async signal
but no idea, why people use the createAsync if it is more limiting in the current version 𖠰🌲 — Yesterday at 22:12 Ahh okay 𖠰🌲 — Yesterday at 22:12 Yeah I'm using it because it's recommended in the docs since it works better with query() I switched back to createResource, however isLoading doesn't become trueeven when the resource is loading, presumably due to the transition being ongoing :/ Any idea how to get the actual value without patching solid router? zulu — Yesterday at 22:15 I have seen that, I also think I saw that they might have patched it to expose the .latest I think
but you are not able to check for the state 𖠰🌲 — Yesterday at 22:15 Yeah .latestis exposed zulu — Yesterday at 22:15 is isLoading exists in the 1.9? 𖠰🌲 — Yesterday at 22:16 Ah sorry I think it's just called resource.loading But it exists in 1.9.5 when using createResource(not createAsync) zulu — Yesterday at 22:20 yes, the createResource has few useful getters
.state .loading .latest .error
but it looks like you are saying that when in transition it does not work as you expect 𖠰🌲 — Yesterday at 22:29 Exactly, here's a reproduction https://stackblitz.com/edit/github-3j8b6an5-tc2z1ort?file=src%2Froutes%2Findex.tsx
Initially when you enter the page it says "We are loading…". When you enter something into the field and press submit
Expected behavior: It should say"We are loading…" again Actual behavior: Nothing happens until 10 seconds later
I cannot use useIsRouting, as I need to know with granularity that exactly this createAsyncis loading. StackBlitz Solid-start Basic Example (forked) - StackBlitz Run official live example code for Solid-start Basic, created by Solidjs on StackBlitz Solid-start Basic Example (forked) - StackBlitz mtt-4242 — Yesterday at 23:17 hi, don't know how much you simplify the example, but i would use an action instead of a query
https://stackblitz.com/edit/github-3j8b6an5-xkvrnurl?file=src%2Froutes%2Findex.tsx
import { action, useAction, useSubmission, } from '@solidjs/router'; import { Show } from 'solid-js';
const doThingOnBackend = action(async (form: FormData) => { 'use server'; // Do heavy processing await new Promise((r) => setTimeout(r, 10_000)); return
We did a lot of thinking about ${form.get('field')} on the backend!; });export default function Home() { const submit = useAction(doThingOnBackend); const submition = useSubmission(doThingOnBackend);
return ( <> <form onSubmit={(e) => { e.preventDefault(); submit(new FormData(e.currentTarget)); }} >
Result of backend process is:{' '} <Show when={submition.pending} fallback={<div style={{ color: 'red' }}>{submition.result}} > Loading... </Show> </> ); } peerreynders — 00:41 You probably want a combination of pending() from useTransition() and useIsRouting()supporthow to trigger <Suspense> on re…
isRouting() only catches navigations but revalidate-ions (which result from actions) of querys also cause transitions. useTransition - SolidDocs Documentation for SolidJS, the signals-powered UI framework useIsRouting - SolidDocs Documentation for SolidJS, the signals-powered UI framework ryansolid — 05:23 Yeah this a good argument for why having a singular global isPending for awkward. That's something that I did differently than React(mostly since we shipped first and this behavior was ambiguous in early React experiments).
In any case dealing with what we have now there are 2 approaches.
Use your own Transition and write to local state and write back to the search params. By awaiting the transition you can set your own isPending Signal on either side and show appropriate UI. This is how isRouting in the router works.
Wrap the Suspense with a keyed Show when={searchParams.userInput}. New Suspense boundaries don't participate in Transitions.
ryansolid — 05:27 I wanted to be careful here. Async isn't transitive in 1.0 and there are some awkward patterns having stuff like .loading and .error lead to. Things not triggering Suspense properly etc. I didn't want to introduce behavior in createAsync we wouldn't keep in 2.0. You can always use createResource. zulu — 05:46 yeah, I understand that the API is not finalized in 2.0
I think the main problem stems from createAsync being preferable / recommended by the docs
I am not sure why the caching improvement was not possible with the existing createResource
but the situation is the until the 2.0 API is settled the 1.0 createAsync might be held back ryansolid — 05:51 I mean technically possible because I made createAsync on createResource, but awkward. Fetching side of createResource doesn't track so it wasn't composable. I wanted to trigger refetching based on keys with a simple wrapper. And createResource meant query woul need to interact with both sides, or be passed via a cache option or something. In 1.0 model stuff like .loading and .error are overused. The only reason for .loading is to guard against transitions to allow tearing. Every other case should just use Suspense. .error was just a mistake. zulu — 05:55 we also have .state I think, why is error a mistake? ryansolid — 05:58 Yeah .state is probably the worst and I was convinced into it as a "why not?". But it leads to a ton of confusion. Because these reads basically guard against Suspense or ErrorBoundary but when something slips through the behavior makes no sense. It also messes with SSR a bit for that reason. Suspense let's us know when parts of the UI are ready. zulu — 06:05 so you mean that the getter allow users to "take over", and by doing that the
is out of the loop for tracking when things are ready. in a sense it allows users to create their own custom Suspense but by doing that it will by pass, the built in functionality that Suspense carry internally
If I understand this correctly ryansolid — 06:08 Yes. Like Streaming SSR, escaping Transitions, deferring mount effects. And its inconsistent especially in terms of error to give this ability just to createResource. zulu — 07:16 @ryansolid does this make sense
how can something that happened in the future ( setTimeout,queueMicrotask) see the past value? when the computed that ran before, already sees the new value.
https://stackblitz.com/edit/github-3j8b6an5-ss9vwqwg?file=src%2Froutes%2Findex.tsx
export default function Home() { const [searchParams, setSearchParams] = useSearchParams(); const backendResult = createAsync(() => { let r = doThingOnBackend(searchParams.userInput as string);
setTimeout(() => { console.log(performance.now()); console.log('setTimeout', searchParams.userInput); }, 0); queueMicrotask(() => { console.log(performance.now()); console.log('queueMicrotask', searchParams.userInput); }); return r;});
createComputed(() => { console.log(performance.now()); console.log('createComputed', searchParams.userInput); });
Image zulu — 07:28 searchParams.userInput is set from 333 to 111 Image zulu — 08:17 Ok, I guess it makes sense, it is just weird the async code can not detect the transaction so we get the non transaction value of the signal. 𖠰🌲 — 14:19 Hmm not sure if this lets me know specifically what createAsync is pending, but thanks 𖠰🌲 — 14:20 Ohh, this is a really nice solution, thank you! Might go with this, I do need it to do an initial submission on pageload though and am not sure if that will work correctly with SSR 𖠰🌲 — 14:22 Ahh makes sense. Yeah I think react router does a thing where it "transitions" (shows old state) for a hardcoded max of like one or two seconds and then still shows the loading UI if queries are pending, that would probably be an acceptable solution for my case if solid also implemented it.
I'll try to understand your solution 1, it might be the best one, since 2. would force everything to be re-created when userInput changes Actually though I think 2 is a really nice an simple solution, since I suppose everything gets re-created anyways when entering the suspense fallback? Going with the keyed show for now and accepting whatever cost it's to re-create my children, thank you all sooooo much for the help! ❤️ I didn't think I'd get away with a two line change for this and was very frustrated
Don't know if you want this issue open to track finding a better solution? But I'm good with the workaround for now.