Suspense don't work when switching routes
Describe the bug
I create routes like this
{
path: "/",
component: (props) => (
<Suspense fallback={<Loading />}>{props.children}</Suspense>
),
children: [
{
path: "",
component: Page,
},
{
path: "test",
component: (props) => (
<Suspense fallback={<Loading1 />}>{props.children}</Suspense>
),
children: [
{
path: "",
component: Page1,
},
],
},
],
}
in Page and Page1's functions, I code like this to show fallback
// src/pages/page.tsx
const Page: Component = () => {
let num = 1;
const [data, { refetch }] = createResource(async () => {
await sleep(1000);
return ++num;
});
return (
<div style={{ display: "flex", gap: "4px" }}>
home-{data()}
<span onClick={refetch}>click to loading</span>
<A href="/test">to /test</A>
{/* in src/pages/test/pages this link is to home and href is '/' */}
</div>
);
};
when I refech and first load Page, Suspense works
but when I go to '/test' then to '/',Suspense doesn't work
the page stay '/test' for 1s, and then turn to '/'
Your Example Website or App
https://codesandbox.io/p/devbox/428phl
Steps to Reproduce the Bug or Issue
- start the project
- click 'click to loading' and find Suspense show fallback
- click 'to /test' and find Suspense show fallback
- click 'to /' and find Suspense doesn't work.
Expected behavior
when swicthing routes. Suspense can always show fallback
Screenshots or Videos
No response
Platform
- OS: Linux( in window docker)
- Browser: Chrome
- Version:
"@solidjs/router": "^0.15.3", "solid-js": "^1.9.3"
Additional context
No response
https://docs.solidjs.com/solid-router/getting-started/config
According to the official documentation, the usage is as follows and this works fine.
function App() {
return (
<Router>
{[
{
path: "/",
component: props => <Suspense fallback={<Loading />}>{props.children}</Suspense>,
children: [
{
path: "",
component: Page
}
]
},
{
path: "/test",
component: props => <Suspense fallback={<Loading1 />}>{props.children}</Suspense>,
children: [
{
path: "",
component: Page1
}
]
}
]}
</Router>
);
}
I think there is a difference between your and my perception of what suspense is used for. Suspense is defined in the official documentation as
A component that tracks all resources read under it and shows a fallback placeholder state until they are resolved.
And in your use case, the move of "/" > "/test" is the action of a parent component making changes to its child component. From the parent <TopSuspense><Page/></TopSuspense> to <TopSuspense><Suspense>{props.children}</Suspense></TopSuspense>.
Once TopSuspense has completed its initialization, it will no longer be in a fallback state unless the suspense is disposed of and remounted. If you want to fallback while changing components between children while the parent component is preserved, a <Show> component would serve the purpose in a case like your page.
If you want to handle suspense as opposed to combining onMount and Show or other components, configure it as a separate route, as in the example at the beginning, instead of connecting it with children.
@dennev I use this code
const [data, { refetch }] = createResource(async () => {
await sleep(1000); // wait 1s
return ++num;
});
to make sure the component will call suspense for 1s. As I think, if the page changes immediateley or suspense is called, it is acceptable. But in fact, everytime switching router, the page will wait 1s and change to new content immediateley.
The problem is that the outer Suspense component isn't being unmounted when you navigate back to the home page (/). It stays in place and simply swaps its child from the inner Suspense to <Page/>. Since the outer Suspense is already active, it doesn't re-trigger its fallback, which is why you don't see the loading state.
Here's a breakdown of what's happening with the code you provided:
Why the Fallback Isn't Working
When you first go to /test from /, the inner Suspense component is mounted for the first time. It sees a resource being fetched inside <Page1/>, so it correctly shows its fallback (<Loading1/>).
However, when you navigate back to /, the outer Suspense component remains in the component tree. Only the inner content changes.
// At /test
<Suspense fallback={<Loading />}> // This component stays mounted
<Suspense fallback={<Loading1 />}> // This component gets unmounted
<Page1 />
</Suspense>
</Suspense>
// Back at /
<Suspense fallback={<Loading />}> // This component is still here
<Page /> // This is a new child, but the parent Suspense doesn't know it needs to show a fallback again
</Suspense>
Because the outer Suspense never gets unmounted, it doesn't see this as a new loading event. It simply updates its children, and the page changes immediately without showing any loading state.
Suspense is a component for initial loading, not a component used to continuously monitor children and display fallbacks. Once the initial rendering is complete, what criteria would be used to trigger the fallback? Every time the child component changes? This could result in the “full page fallback” screen being called even for minor changes, which is likely to be unintended behavior. Additionally, while this is currently a simple repetition of suspense, such cases could arise.
<Suspense fallback={<Loading />}> // This component stays mounted
<h>Hola!</h>
<div><MyDescription/></h>
<Suspense fallback={<Loading1 />}> // This component gets unmounted
<ChartComponent />
</Suspense>
</Suspense>
Here, during initial loading, you want to show a clean full screen, so you use Suspense to show a fallback, but let's say that once it's loaded, you want to keep showing the overview even if a few miscellaneous components at the bottom of the screen change. In this case, if suspense tracks whether child components are loaded, the full-screen fallback will continue to be triggered.
Based on your example, a simple modification would be to change app.tsx as follows.
import { lazy, Suspense } from "solid-js";
import { Router } from "@solidjs/router";
import Loading from "./pages/loading";
const Page = lazy(() => import("./pages/page"));
const Loading1 = lazy(() => import("./pages/test/loading"));
const Page1 = lazy(() => import("./pages/test/page"));
function App() {
return (
<Router>
{{
path: "/",
component: (props) => (
<Suspense fallback={<Loading />}> // < Change from here!
<div>
<p>Hola!</p>
{props.children}
</div>
</Suspense> // End of the Suspense component to be modified
),
children: [
{
path: "",
component: Page,
},
{
path: "test",
component: (props) => (
<Suspense fallback={<Loading1 />}>{props.children}</Suspense>
),
children: [
{
path: "",
component: Page1,
},
],
},
],
}}
</Router>
);
}
export default App;
@dennev It seems like you don't understand what happens.
try to click this then you will see fallback , what means showing fallback never needs to destory Suspense.
Now if you click 'to /' from '/test', a number 1 will be printed immediately and the page changes after 5s.
The component Page is destoryed and re-created, and createSource should call Suspense and it doesn't .
I change the code in
src/pages/page.tsx.
As you said, it is a <Page/> component. It is a component that is unMounted/reMounted, and the top-level <Suspense> remains as the parent component.
Please review the code block in this response.
https://github.com/solidjs/solid-router/issues/521#issuecomment-3173112525