solid-router icon indicating copy to clipboard operation
solid-router copied to clipboard

Suspense don't work when switching routes

Open FanetheDivine opened this issue 8 months ago • 6 comments

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

  1. start the project
  2. click 'click to loading' and find Suspense show fallback
  3. click 'to /test' and find Suspense show fallback
  4. 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

FanetheDivine avatar Apr 06 '25 05:04 FanetheDivine

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 avatar Aug 10 '25 15:08 dennev

@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.

FanetheDivine avatar Aug 11 '25 01:08 FanetheDivine

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.

dennev avatar Aug 11 '25 02:08 dennev

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 avatar Aug 11 '25 02:08 dennev

@dennev It seems like you don't understand what happens.

Image

try to click this then you will see fallback , what means showing fallback never needs to destory Suspense.

Image I change the code in `src/pages/page.tsx` .

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 .

FanetheDivine avatar Aug 11 '25 02:08 FanetheDivine

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

dennev avatar Aug 11 '25 03:08 dennev